From c8f79abd9ba2af54f0a42231360ba85983c209cb Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 24 Oct 2025 16:07:02 +0200 Subject: [PATCH 01/25] remove dependency on bouncycastle by internalizing CMAC implementation --- pom.xml | 83 ++++---- src/main/java/org/cryptomator/siv/CMac.java | 186 ++++++++++++++++++ .../cryptomator/siv/CustomCtrComputer.java | 37 ---- .../cryptomator/siv/JceAesBlockCipher.java | 116 ----------- .../java/org/cryptomator/siv/SivMode.java | 105 +++------- .../org/bouncycastle/crypto/Placeholder.java | 8 - .../bouncycastle/crypto/macs/Placeholder.java | 8 - .../crypto/modes/Placeholder.java | 8 - .../crypto/paddings/Placeholder.java | 8 - .../crypto/params/Placeholder.java | 8 - .../org/bouncycastle/util/Placeholder.java | 8 - src/main/java9/module-info.java | 7 - .../org/cryptomator/siv/BenchmarkTest.java | 8 - .../java/org/cryptomator/siv/CMacTest.java | 87 ++++++++ .../siv/CustomCtrComputerTest.java | 66 ------- .../siv/JceAesBlockCipherTest.java | 173 ---------------- .../org/cryptomator/siv/SivModeBenchmark.java | 46 ----- .../java/org/cryptomator/siv/SivModeTest.java | 27 --- 18 files changed, 328 insertions(+), 661 deletions(-) create mode 100644 src/main/java/org/cryptomator/siv/CMac.java delete mode 100644 src/main/java/org/cryptomator/siv/CustomCtrComputer.java delete mode 100644 src/main/java/org/cryptomator/siv/JceAesBlockCipher.java delete mode 100644 src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/Placeholder.java delete mode 100644 src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/macs/Placeholder.java delete mode 100644 src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/modes/Placeholder.java delete mode 100644 src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/paddings/Placeholder.java delete mode 100644 src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/params/Placeholder.java delete mode 100644 src/main/java/org/cryptomator/siv/org/bouncycastle/util/Placeholder.java create mode 100644 src/test/java/org/cryptomator/siv/CMacTest.java delete mode 100644 src/test/java/org/cryptomator/siv/CustomCtrComputerTest.java delete mode 100644 src/test/java/org/cryptomator/siv/JceAesBlockCipherTest.java diff --git a/pom.xml b/pom.xml index 539bb37..3d10e06 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 org.cryptomator siv-mode @@ -37,28 +38,21 @@ UTF-8 2025-03-14T12:02:43Z - - 1.80 - 5.12.0 - 5.15.2 + 5.20.0 1.37 3.0 33.4.0-jre 12.1.1 + + + - - org.bouncycastle - bcprov-jdk18on - ${bouncycastle.version} - - true - org.jetbrains annotations @@ -134,6 +128,19 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + jar-paths-to-properties + validate + + properties + + + + maven-compiler-plugin 3.14.0 @@ -141,6 +148,13 @@ 8 UTF-8 true + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + @@ -163,6 +177,9 @@ org.apache.maven.plugins maven-surefire-plugin 3.5.3 + + @{surefire.jacoco.args} -javaagent:${org.mockito:mockito-core:jar} + maven-jar-plugin @@ -208,43 +225,6 @@ 8 - - maven-shade-plugin - 3.6.0 - - - package - - shade - - - true - false - false - false - - - org.bouncycastle:bcprov-jdk18on - - - - - org.bouncycastle - org.cryptomator.siv.org.bouncycastle - - - - - org.bouncycastle:bcprov-jdk18on - - META-INF/** - - - - - - - @@ -292,6 +272,9 @@ prepare-agent + + surefire.jacoco.args + @@ -340,7 +323,7 @@ true central - true + true diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java new file mode 100644 index 0000000..20493f0 --- /dev/null +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -0,0 +1,186 @@ +package org.cryptomator.siv; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.MacSpi; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; + +/** + * AES-CMAC (Cipher-based Message Authentication Code). + * Specs: RFC 4493. + */ +class CMac extends MacSpi { + + private static final int BLOCK_SIZE = 16; // 128 bits for AES + private static final String AES_ALGORITHM = "AES"; + private static final String AES_ECB_NO_PADDING = "AES/ECB/NoPadding"; + + // MAC keys: + private Cipher cipher; + private byte[] k1; + private byte[] k2; + + // MAC state: + private int bufferPos = 0; + private byte[] buffer = new byte[BLOCK_SIZE]; + private byte[] x = new byte[BLOCK_SIZE]; // X := const_Zero; + private byte[] y = new byte[BLOCK_SIZE]; + private int msgLen = 0; + + @Override + protected int engineGetMacLength() { + return BLOCK_SIZE; + } + + @Override + protected void engineInit(Key key, AlgorithmParameterSpec params) throws InvalidKeyException { + try { + this.cipher = Cipher.getInstance(AES_ECB_NO_PADDING); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new AssertionError("Every implementation of the Java platform is required to support [...] AES/ECB/NoPadding", e); + } + cipher.init(Cipher.ENCRYPT_MODE, key); + + // init subkeys K1 and K2 + // see https://www.rfc-editor.org/rfc/rfc4493.html#section-2.3 + byte[] L = new byte[BLOCK_SIZE]; + try { + // L = AES_encrypt(K, const_Zero) + L = encryptBlock(cipher, L); + this.k1 = SivMode.dbl(L); + this.k2 = SivMode.dbl(k1); + } finally { + Arrays.fill(L, (byte) 0); + } + } + + @Override + protected void engineUpdate(byte input) { + if (bufferPos == BLOCK_SIZE) { // buffer is full + processBlock(); + } + assert bufferPos < BLOCK_SIZE; + buffer[bufferPos++] = input; + msgLen++; + } + + @Override + protected void engineUpdate(byte[] input, int offset, int len) { + assert bufferPos < BLOCK_SIZE; + for (int i = offset; i < offset + len; ) { + if (bufferPos == BLOCK_SIZE) { // buffer is full + processBlock(); + } + int required = offset + len - i; + int available = BLOCK_SIZE - bufferPos; + int m = Math.min(required, available); + System.arraycopy(input, i, buffer, bufferPos, m); + bufferPos += m; + i += m; + } + msgLen += len; + } + + // https://www.rfc-editor.org/rfc/rfc4493.html#section-2.4 Step 6 + private void processBlock() { + y = SivMode.xor(x, buffer); // Y := X XOR M_i; + x = encryptBlock(cipher, y); // X := AES-128(K,Y); + bufferPos = 0; + } + + // https://www.rfc-editor.org/rfc/rfc4493.html#section-2.4 + @Override + protected byte[] engineDoFinal() { + // Step 3: + boolean flag = msgLen > 0 && bufferPos % BLOCK_SIZE == 0; // denoting if last block is complete or not + + // Step 4: + byte[] m_last; + if (flag) { + // M_last := M_n XOR K1; + m_last = SivMode.xor(buffer, k1); + } else { + // M_last := padding(M_n) XOR K2; + // + // [...] padding(x) is the concatenation of x and a single '1', + // followed by the minimum number of '0's, so that the total length is + // equal to 128 bits. + buffer[bufferPos] = (byte) 0x80; // single '1' bit + if (bufferPos + 1 < BLOCK_SIZE) { + Arrays.fill(buffer, bufferPos + 1, BLOCK_SIZE, (byte) 0x00); // followed by '0' bits + } + m_last = SivMode.xor(buffer, k2); + } + + // Step 7: + y = SivMode.xor(m_last, x); // Y := M_last XOR X; + try { + return encryptBlock(cipher, y); // T := AES-128(K,Y); + } finally { + engineReset(); + } + } + + @Override + protected void engineReset() { + bufferPos = 0; + msgLen = 0; + Arrays.fill(buffer, (byte) 0); + Arrays.fill(x, (byte) 0); + Arrays.fill(y, (byte) 0); + } + + // TODO make instance method, remove cipher param? + private static byte[] encryptBlock(Cipher cipher, byte[] block) { + try { + return cipher.doFinal(block); + } catch (IllegalBlockSizeException e) { + throw new IllegalArgumentException(e); + } catch (BadPaddingException e) { + throw new AssertionError("Not in decrypt mode", e); + } + } + + /** + * Create a new CMAC instance for incremental message processing + */ + public static CMac create(byte[] key) { + if (key.length != 16 && key.length != 24 && key.length != 32) { + throw new IllegalArgumentException("Invalid key length. Must be 16, 24, or 32 bytes"); + } + try { + SecretKeySpec keySpec = new SecretKeySpec(key, AES_ALGORITHM); + + CMac mac = new CMac(); + mac.engineInit(keySpec, null); + return mac; + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("Invalid key", e); + } + } + + /** + * One-shot CMAC computation + */ + public static byte[] tag(byte[] key, byte[] message) { + CMac cmac = create(key); + cmac.engineUpdate(message, 0, message.length); + return cmac.engineDoFinal(); + } + + /** + * Verify CMAC tag + */ + public static boolean verify(byte[] key, byte[] message, byte[] tag) { + byte[] computedTag = tag(key, message); + return MessageDigest.isEqual(computedTag, tag); + } +} diff --git a/src/main/java/org/cryptomator/siv/CustomCtrComputer.java b/src/main/java/org/cryptomator/siv/CustomCtrComputer.java deleted file mode 100644 index 043aa7b..0000000 --- a/src/main/java/org/cryptomator/siv/CustomCtrComputer.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.cryptomator.siv; - -import org.bouncycastle.crypto.BlockCipher; -import org.bouncycastle.crypto.CipherParameters; -import org.bouncycastle.crypto.OutputLengthException; -import org.bouncycastle.crypto.modes.CTRModeCipher; -import org.bouncycastle.crypto.modes.SICBlockCipher; -import org.bouncycastle.crypto.params.KeyParameter; -import org.bouncycastle.crypto.params.ParametersWithIV; - -import java.util.function.Supplier; - -/** - * Performs CTR Mode computations facilitating BouncyCastle's {@link SICBlockCipher}. - */ -class CustomCtrComputer implements SivMode.CtrComputer { - - private final Supplier blockCipherSupplier; - - public CustomCtrComputer(Supplier blockCipherSupplier) { - this.blockCipherSupplier = blockCipherSupplier; - } - - @Override - public byte[] computeCtr(byte[] input, byte[] key, byte[] iv) { - CTRModeCipher cipher = SICBlockCipher.newInstance(blockCipherSupplier.get()); - CipherParameters params = new ParametersWithIV(new KeyParameter(key), iv); - cipher.init(true, params); - try { - byte[] output = new byte[input.length]; - cipher.processBytes(input, 0, input.length, output, 0); - return output; - } catch (OutputLengthException e) { - throw new IllegalStateException("In CTR mode output length must be equal to input length", e); - } - } -} diff --git a/src/main/java/org/cryptomator/siv/JceAesBlockCipher.java b/src/main/java/org/cryptomator/siv/JceAesBlockCipher.java deleted file mode 100644 index 371b739..0000000 --- a/src/main/java/org/cryptomator/siv/JceAesBlockCipher.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.cryptomator.siv; -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - ******************************************************************************/ - -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.Provider; - -import javax.crypto.Cipher; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.ShortBufferException; -import javax.crypto.spec.SecretKeySpec; - -import org.bouncycastle.crypto.BlockCipher; -import org.bouncycastle.crypto.CipherParameters; -import org.bouncycastle.crypto.DataLengthException; -import org.bouncycastle.crypto.params.KeyParameter; - -/** - * Adapter class between BouncyCastle's {@link BlockCipher} and JCE's {@link Cipher} API. - * - *

- * As per contract of {@link BlockCipher#processBlock(byte[], int, byte[], int)}, this class is designed to encrypt or decrypt just one single block at a time. - * JCE doesn't allow us to retrieve the plain cipher without a mode, so we explicitly request {@value #SINGLE_BLOCK_PLAIN_AES_JCE_CIPHER_NAME}. - * This is by design, because we want the plain cipher for a single 128 bit block without any mode. We're not actually using ECB mode. - * - *

- * This is a package-private class only used to encrypt the 128 bit counter during SIV mode. - */ -final class JceAesBlockCipher implements BlockCipher { - - private static final String ALG_NAME = "AES"; - private static final String KEY_DESIGNATION = "AES"; - private static final String SINGLE_BLOCK_PLAIN_AES_JCE_CIPHER_NAME = "AES/ECB/NoPadding"; - - private final Cipher cipher; - private Key key; - private int opmode; - - JceAesBlockCipher() { - this(null); - } - - JceAesBlockCipher(Provider provider) { - try { - if(provider != null) { - this.cipher = Cipher.getInstance(SINGLE_BLOCK_PLAIN_AES_JCE_CIPHER_NAME, provider); - } else { - this.cipher = Cipher.getInstance(SINGLE_BLOCK_PLAIN_AES_JCE_CIPHER_NAME); // defaults to SunJCE - } - } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - throw new IllegalStateException("Every implementation of the Java platform is required to support AES/ECB/NoPadding."); - } - } - - @Override - public void init(boolean forEncryption, CipherParameters params) throws IllegalArgumentException { - if (params instanceof KeyParameter) { - init(forEncryption, (KeyParameter) params); - } else { - throw new IllegalArgumentException("Invalid or missing parameter of type KeyParameter."); - } - } - - private void init(boolean forEncryption, KeyParameter keyParam) throws IllegalArgumentException { - this.key = new SecretKeySpec(keyParam.getKey(), KEY_DESIGNATION); - this.opmode = forEncryption ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE; - try { - cipher.init(opmode, key); - } catch (InvalidKeyException e) { - throw new IllegalArgumentException("Invalid key.", e); - } - } - - @Override - public String getAlgorithmName() { - return ALG_NAME; - } - - @Override - public int getBlockSize() { - return cipher.getBlockSize(); - } - - @Override - public int processBlock(byte[] in, int inOff, byte[] out, int outOff) throws DataLengthException, IllegalStateException { - if (in.length - inOff < getBlockSize()) { - throw new DataLengthException("Insufficient data in 'in'."); - } - try { - return cipher.update(in, inOff, getBlockSize(), out, outOff); - } catch (ShortBufferException e) { - throw new DataLengthException("Insufficient space in 'out'."); - } - } - - @Override - public void reset() { - if (key == null) { - return; // no-op if init has not been called yet. - } - try { - cipher.init(opmode, key); - } catch (InvalidKeyException e) { - throw new IllegalStateException("cipher.init(...) already invoked successfully earlier with same parameters."); - } - } - -} diff --git a/src/main/java/org/cryptomator/siv/SivMode.java b/src/main/java/org/cryptomator/siv/SivMode.java index 3f48c55..ae84dc9 100644 --- a/src/main/java/org/cryptomator/siv/SivMode.java +++ b/src/main/java/org/cryptomator/siv/SivMode.java @@ -1,19 +1,5 @@ package org.cryptomator.siv; -/******************************************************************************* - * Copyright (c) 2015 Sebastian Stenzel - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - ******************************************************************************/ - -import org.bouncycastle.crypto.BlockCipher; -import org.bouncycastle.crypto.CipherParameters; -import org.bouncycastle.crypto.Mac; -import org.bouncycastle.crypto.macs.CMac; -import org.bouncycastle.crypto.paddings.ISO7816d4Padding; -import org.bouncycastle.crypto.params.KeyParameter; + import org.jetbrains.annotations.VisibleForTesting; import javax.crypto.IllegalBlockSizeException; @@ -29,71 +15,26 @@ public final class SivMode { private static final byte[] BYTES_ZERO = new byte[16]; private static final byte DOUBLING_CONST = (byte) 0x87; - private final ThreadLocal threadLocalCipher; private final CtrComputer ctrComputer; /** * Creates an AES-SIV instance using JCE's cipher implementation, which should normally be the best choice.
- *

- * For embedded systems, you might want to consider using {@link #SivMode(BlockCipherFactory)} with BouncyCastle's {@code AESLightEngine} instead. - * - * @see #SivMode(BlockCipherFactory) */ public SivMode() { - this((Provider) null); + this( null); } /** * Creates an AES-SIV instance using a custom JCE's security provider
- *

- * For embedded systems, you might want to consider using {@link #SivMode(BlockCipherFactory)} with BouncyCastle's {@code AESLightEngine} instead. * * @param jceSecurityProvider to use to create the internal {@link javax.crypto.Cipher} instance - * @see #SivMode(BlockCipherFactory) */ public SivMode(final Provider jceSecurityProvider) { - this(ThreadLocals.withInitial(() -> new JceAesBlockCipher(jceSecurityProvider)), new JceAesCtrComputer(jceSecurityProvider)); + this.ctrComputer = new JceAesCtrComputer(jceSecurityProvider); } /** - * Creates an instance using a specific Blockcipher.get(). If you want to use AES, just use the default constructor. - * - * @param cipherFactory A factory method creating a Blockcipher.get(). Must use a block size of 128 bits (16 bytes). - */ - public SivMode(final BlockCipherFactory cipherFactory) { - this(ThreadLocals.withInitial(cipherFactory::create)); - } - - private SivMode(final ThreadLocal threadLocalCipher) { - this(threadLocalCipher, new CustomCtrComputer(threadLocalCipher::get)); - } - - private SivMode(final ThreadLocal threadLocalCipher, final CtrComputer ctrComputer) { - // Try using cipherFactory to check that the block size is valid. - // We assume here that the block size will not vary across calls to .create(). - if (threadLocalCipher.get().getBlockSize() != 16) { - throw new IllegalArgumentException("cipherFactory must create BlockCipher objects with a 16-byte block size"); - } - - this.threadLocalCipher = threadLocalCipher; - this.ctrComputer = ctrComputer; - } - - /** - * Creates {@link BlockCipher}s. - */ - @FunctionalInterface - public interface BlockCipherFactory { - /** - * Creates a new {@link BlockCipher}. - * - * @return New {@link BlockCipher} instance - */ - BlockCipher create(); - } - - /** - * Performs CTR computations. + * Performs CTR computations. */ @FunctionalInterface interface CtrComputer { @@ -102,8 +43,9 @@ interface CtrComputer { /** * 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 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. */ @@ -141,7 +83,7 @@ public byte[] encrypt(SecretKey ctrKey, SecretKey macKey, byte[] plaintext, byte * @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. - * @throws IllegalArgumentException if the either of the two keys is of invalid length for the used {@link BlockCipher}. + * @throws IllegalArgumentException if the either of the two keys is of invalid length. */ public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... associatedData) { // Check if plaintext length will cause overflows @@ -161,8 +103,9 @@ public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... /** * 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 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. @@ -197,7 +140,7 @@ public byte[] decrypt(SecretKey ctrKey, SecretKey macKey, byte[] ciphertext, byt * @param ciphertext Your ciphertext, which shall be encrypted. * @param associatedData Optional associated data, which needs to be authenticated during decryption. * @return Plaintext byte array. - * @throws IllegalArgumentException If the either of the two keys is of invalid length for the used {@link BlockCipher}. + * @throws IllegalArgumentException If the either of the two keys is of invalid length. * @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. */ @@ -295,7 +238,7 @@ byte[] computeCtr(byte[] input, byte[] key, final byte[] iv) { final byte[] adjustedIv = Arrays.copyOf(iv, 16); adjustedIv[8] = (byte) (adjustedIv[8] & 0x7F); adjustedIv[12] = (byte) (adjustedIv[12] & 0x7F); - + return ctrComputer.computeCtr(input, key, adjustedIv); } @@ -307,19 +250,17 @@ byte[] s2v(byte[] macKey, byte[] plaintext, byte[]... associatedData) throws Ill throw new IllegalArgumentException("too many Associated Data fields"); } - final CipherParameters params = new KeyParameter(macKey); - final CMac mac = new CMac(threadLocalCipher.get()); - mac.init(params); - // RFC 5297 defines a n == 0 case here. Where n is the length of the input vector: // S1 = associatedData1, S2 = associatedData2, ... Sn = plaintext // Since this method is invoked only by encrypt/decrypt, we always have a plaintext. // Thus n > 0 - byte[] d = mac(mac, BYTES_ZERO); + CMac cmac = CMac.create(macKey); + + byte[] d = mac(cmac, BYTES_ZERO); for (byte[] s : associatedData) { - d = xor(dbl(d), mac(mac, s)); + d = xor(dbl(d), mac(cmac, s)); } final byte[] t; @@ -329,20 +270,18 @@ byte[] s2v(byte[] macKey, byte[] plaintext, byte[]... associatedData) throws Ill t = xor(dbl(d), pad(plaintext)); } - return mac(mac, t); + return mac(cmac, t); } - private static byte[] mac(Mac mac, byte[] in) { - byte[] result = new byte[mac.getMacSize()]; - mac.update(in, 0, in.length); - mac.doFinal(result, 0); - return result; + private static byte[] mac(CMac mac, byte[] in) { + mac.engineUpdate(in, 0, in.length); + return mac.engineDoFinal(); } // First bit 1, following bits 0. private static byte[] pad(byte[] in) { final byte[] result = Arrays.copyOf(in, 16); - new ISO7816d4Padding().addPadding(result, in.length); + result[in.length] = (byte) 0x80; return result; } diff --git a/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/Placeholder.java b/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/Placeholder.java deleted file mode 100644 index 4f8443c..0000000 --- a/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/Placeholder.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.cryptomator.siv.org.bouncycastle.crypto; - -/** - * module-info will be evaluated before maven-shade-plugin, so we need this placeholder - * to avoid complaints about this package being empty. - */ -interface Placeholder { -} diff --git a/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/macs/Placeholder.java b/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/macs/Placeholder.java deleted file mode 100644 index b6b7184..0000000 --- a/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/macs/Placeholder.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.cryptomator.siv.org.bouncycastle.crypto.macs; - -/** - * module-info will be evaluated before maven-shade-plugin, so we need this placeholder - * to avoid complaints about this package being empty. - */ -interface Placeholder { -} diff --git a/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/modes/Placeholder.java b/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/modes/Placeholder.java deleted file mode 100644 index a5748f3..0000000 --- a/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/modes/Placeholder.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.cryptomator.siv.org.bouncycastle.crypto.modes; - -/** - * module-info will be evaluated before maven-shade-plugin, so we need this placeholder - * to avoid complaints about this package being empty. - */ -interface Placeholder { -} diff --git a/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/paddings/Placeholder.java b/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/paddings/Placeholder.java deleted file mode 100644 index 0ddc77a..0000000 --- a/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/paddings/Placeholder.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.cryptomator.siv.org.bouncycastle.crypto.paddings; - -/** - * module-info will be evaluated before maven-shade-plugin, so we need this placeholder - * to avoid complaints about this package being empty. - */ -interface Placeholder { -} diff --git a/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/params/Placeholder.java b/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/params/Placeholder.java deleted file mode 100644 index d1ca382..0000000 --- a/src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/params/Placeholder.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.cryptomator.siv.org.bouncycastle.crypto.params; - -/** - * module-info will be evaluated before maven-shade-plugin, so we need this placeholder - * to avoid complaints about this package being empty. - */ -interface Placeholder { -} diff --git a/src/main/java/org/cryptomator/siv/org/bouncycastle/util/Placeholder.java b/src/main/java/org/cryptomator/siv/org/bouncycastle/util/Placeholder.java deleted file mode 100644 index e0f1250..0000000 --- a/src/main/java/org/cryptomator/siv/org/bouncycastle/util/Placeholder.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.cryptomator.siv.org.bouncycastle.util; - -/** - * module-info will be evaluated before maven-shade-plugin, so we need this placeholder - * to avoid complaints about this package being empty. - */ -interface Placeholder { -} diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java index c8ce74e..98a03f6 100644 --- a/src/main/java9/module-info.java +++ b/src/main/java9/module-info.java @@ -1,12 +1,5 @@ module org.cryptomator.siv { - requires static org.bouncycastle.provider; requires static org.jetbrains.annotations; exports org.cryptomator.siv; - exports org.cryptomator.siv.org.bouncycastle.crypto; - exports org.cryptomator.siv.org.bouncycastle.crypto.macs; - exports org.cryptomator.siv.org.bouncycastle.crypto.modes; - exports org.cryptomator.siv.org.bouncycastle.crypto.paddings; - exports org.cryptomator.siv.org.bouncycastle.crypto.params; - exports org.cryptomator.siv.org.bouncycastle.util; } \ No newline at end of file diff --git a/src/test/java/org/cryptomator/siv/BenchmarkTest.java b/src/test/java/org/cryptomator/siv/BenchmarkTest.java index a2d3126..69be842 100644 --- a/src/test/java/org/cryptomator/siv/BenchmarkTest.java +++ b/src/test/java/org/cryptomator/siv/BenchmarkTest.java @@ -1,11 +1,3 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - ******************************************************************************/ package org.cryptomator.siv; import org.junit.jupiter.api.Test; diff --git a/src/test/java/org/cryptomator/siv/CMacTest.java b/src/test/java/org/cryptomator/siv/CMacTest.java new file mode 100644 index 0000000..bbff6ea --- /dev/null +++ b/src/test/java/org/cryptomator/siv/CMacTest.java @@ -0,0 +1,87 @@ +package org.cryptomator.siv; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.converter.ArgumentConversionException; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.converter.SimpleArgumentConverter; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CMacTest { + + /** + * Convert hex string to byte array + */ + static class HexConverter extends SimpleArgumentConverter { + @Override + protected Object convert(Object source, Class targetType) throws ArgumentConversionException { + if (source == null) { + return new byte[0]; + } else if (!(source instanceof String)) { + throw new ArgumentConversionException("Source must be a String"); + } + String hex = (String) source; + if (hex.isEmpty()) { + return new byte[0]; + } + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i + 1), 16)); + } + return data; + } + } + + /** + * Convert byte array to hex string + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); + } + + @ParameterizedTest(name = "{0}") + @CsvSource(delimiterString = "|", value = { + "Test Case 1: Empty message | 2b7e151628aed2a6abf7158809cf4f3c | | bb1d6929e95937287fa37d129b756746", + "Test Case 2: 16-byte message | 2b7e151628aed2a6abf7158809cf4f3c | 6bc1bee22e409f96e93d7e117393172a | 070a16b46b4d4144f79bdd9dd04a287c", + "Test Case 3: 40-byte message | 2b7e151628aed2a6abf7158809cf4f3c | 6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411 | dfa66747de9ae63030ca32611497c827", + "Test Case 4: 64-byte message | 2b7e151628aed2a6abf7158809cf4f3c | 6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710 | 51f0bebf7e3b9d92fc49741779363cfe" + }) + void testRfc4493Vectors(String testName, + @ConvertWith(HexConverter.class) byte[] key, + @ConvertWith(HexConverter.class) byte[] message, + String expectedHex) { + byte[] result = CMac.tag(key, message); + assertEquals(expectedHex, bytesToHex(result), testName + " failed"); + } + + @ParameterizedTest(name = "{0}") + @CsvSource(delimiterString = "|", value = { + "AES-128 Example 1 | 2B7E151628AED2A6ABF7158809CF4F3C | | bb1d6929e95937287fa37d129b756746", + "AES-128 Example 2 | 2B7E151628AED2A6ABF7158809CF4F3C | 6BC1BEE22E409F96E93D7E117393172A | 070a16b46b4d4144f79bdd9dd04a287c", + "AES-128 Example 3 | 2B7E151628AED2A6ABF7158809CF4F3C | 6BC1BEE22E409F96E93D7E117393172AAE2D8A57 | 7d85449ea6ea19c823a7bf78837dfade", + "AES-128 Example 4 | 2B7E151628AED2A6ABF7158809CF4F3C | 6BC1BEE22E409F96E93D7E117393172AAE2D8A571E03AC9C9EB76FAC45AF8E5130C81C46A35CE411E5FBC1191A0A52EFF69F2445DF4F9B17AD2B417BE66C3710 | 51f0bebf7e3b9d92fc49741779363cfe", + "AES-192 Example 1 | 8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B | | d17ddf46adaacde531cac483de7a9367", + "AES-192 Example 2 | 8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B | 6BC1BEE22E409F96E93D7E117393172A | 9e99a7bf31e710900662f65e617c5184", + "AES-192 Example 3 | 8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B | 6BC1BEE22E409F96E93D7E117393172AAE2D8A57 | 3d75c194ed96070444a9fa7ec740ecf8", + "AES-192 Example 4 | 8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B | 6BC1BEE22E409F96E93D7E117393172AAE2D8A571E03AC9C9EB76FAC45AF8E5130C81C46A35CE411E5FBC1191A0A52EFF69F2445DF4F9B17AD2B417BE66C3710 | a1d5df0eed790f794d77589659f39a11", + "AES-256 Example 1 | 603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4 | | 028962f61b7bf89efc6b551f4667d983", + "AES-256 Example 2 | 603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4 | 6BC1BEE22E409F96E93D7E117393172A | 28a7023f452e8f82bd4bf28d8c37c35c", + "AES-256 Example 3 | 603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4 | 6BC1BEE22E409F96E93D7E117393172AAE2D8A57 | 156727dc0878944a023c1fe03bad6d93", + "AES-256 Example 4 | 603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4 | 6BC1BEE22E409F96E93D7E117393172AAE2D8A571E03AC9C9EB76FAC45AF8E5130C81C46A35CE411E5FBC1191A0A52EFF69F2445DF4F9B17AD2B417BE66C3710 | e1992190549f6ed5696a2c056c315410" + }) + void testNistVectors(String testName, + @ConvertWith(HexConverter.class) byte[] key, + @ConvertWith(HexConverter.class) byte[] message, + String expectedHex) { + byte[] result = CMac.tag(key, message); + assertEquals(expectedHex, bytesToHex(result), testName + " failed"); + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/siv/CustomCtrComputerTest.java b/src/test/java/org/cryptomator/siv/CustomCtrComputerTest.java deleted file mode 100644 index 301dfcb..0000000 --- a/src/test/java/org/cryptomator/siv/CustomCtrComputerTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.cryptomator.siv; - -import org.bouncycastle.crypto.BlockCipher; -import org.bouncycastle.crypto.engines.AESLightEngine; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class CustomCtrComputerTest { - - private BlockCipher supplyBlockCipher() { - return new AESLightEngine(); - } - - // CTR-AES https://tools.ietf.org/html/rfc5297#appendix-A.1 - @Test - public void testComputeCtr1() { - byte[] ctrKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, // - (byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, // - (byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, // - (byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff}; - - byte[] ctr = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // - (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // - (byte) 0x15, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // - (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93}; - - byte[] expected = {(byte) 0x51, (byte) 0xe2, (byte) 0x18, (byte) 0xd2, // - (byte) 0xc5, (byte) 0xa2, (byte) 0xab, (byte) 0x8c, // - (byte) 0x43, (byte) 0x45, (byte) 0xc4, (byte) 0xa6, // - (byte) 0x23, (byte) 0xb2, (byte) 0xf0, (byte) 0x8f}; - - byte[] result = new CustomCtrComputer(this::supplyBlockCipher).computeCtr(new byte[16], ctrKey, ctr); - Assertions.assertArrayEquals(expected, result); - } - - // CTR-AES https://tools.ietf.org/html/rfc5297#appendix-A.2 - @Test - public void testComputeCtr2() { - final byte[] ctrKey = {(byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, // - (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, // - (byte) 0x48, (byte) 0x49, (byte) 0x4a, (byte) 0x4b, // - (byte) 0x4c, (byte) 0x4d, (byte) 0x4e, (byte) 0x4f}; - - final byte[] ctr = {(byte) 0x7b, (byte) 0xdb, (byte) 0x6e, (byte) 0x3b, // - (byte) 0x43, (byte) 0x26, (byte) 0x67, (byte) 0xeb, // - (byte) 0x06, (byte) 0xf4, (byte) 0xd1, (byte) 0x4b, // - (byte) 0x7f, (byte) 0x2f, (byte) 0xbd, (byte) 0x0f}; - - final byte[] expected = {(byte) 0xbf, (byte) 0xf8, (byte) 0x66, (byte) 0x5c, // - (byte) 0xfd, (byte) 0xd7, (byte) 0x33, (byte) 0x63, // - (byte) 0x55, (byte) 0x0f, (byte) 0x74, (byte) 0x00, // - (byte) 0xe8, (byte) 0xf9, (byte) 0xd3, (byte) 0x76, // - (byte) 0xb2, (byte) 0xc9, (byte) 0x08, (byte) 0x8e, // - (byte) 0x71, (byte) 0x3b, (byte) 0x86, (byte) 0x17, // - (byte) 0xd8, (byte) 0x83, (byte) 0x92, (byte) 0x26, // - (byte) 0xd9, (byte) 0xf8, (byte) 0x81, (byte) 0x59, // - (byte) 0x9e, (byte) 0x44, (byte) 0xd8, (byte) 0x27, // - (byte) 0x23, (byte) 0x49, (byte) 0x49, (byte) 0xbc, // - (byte) 0x1b, (byte) 0x12, (byte) 0x34, (byte) 0x8e, // - (byte) 0xbc, (byte) 0x19, (byte) 0x5e, (byte) 0xc7}; - - byte[] result = new CustomCtrComputer(this::supplyBlockCipher).computeCtr(new byte[48], ctrKey, ctr); - Assertions.assertArrayEquals(expected, result); - } - -} diff --git a/src/test/java/org/cryptomator/siv/JceAesBlockCipherTest.java b/src/test/java/org/cryptomator/siv/JceAesBlockCipherTest.java deleted file mode 100644 index e02a211..0000000 --- a/src/test/java/org/cryptomator/siv/JceAesBlockCipherTest.java +++ /dev/null @@ -1,173 +0,0 @@ -package org.cryptomator.siv; -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - ******************************************************************************/ - -import org.bouncycastle.crypto.CipherParameters; -import org.bouncycastle.crypto.DataLengthException; -import org.bouncycastle.crypto.params.AsymmetricKeyParameter; -import org.bouncycastle.crypto.params.KeyParameter; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.security.Provider; -import java.security.Security; - -public class JceAesBlockCipherTest { - - @Test - public void testInitWithNullParam() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - CipherParameters params = null; - IllegalArgumentException e = Assertions.assertThrows(IllegalArgumentException.class, () -> { - cipher.init(true, params); - }); - MatcherAssert.assertThat(e.getMessage(), CoreMatchers.containsString("missing parameter of type KeyParameter")); - } - - @Test - public void testInitWithMissingKey() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - CipherParameters params = new AsymmetricKeyParameter(true); - IllegalArgumentException e = Assertions.assertThrows(IllegalArgumentException.class, () -> { - cipher.init(true, params); - }); - MatcherAssert.assertThat(e.getMessage(), CoreMatchers.containsString("missing parameter of type KeyParameter")); - } - - @Test - public void testInitWithInvalidKey() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - CipherParameters params = new KeyParameter(new byte[7]); - IllegalArgumentException e = Assertions.assertThrows(IllegalArgumentException.class, () -> { - cipher.init(true, params); - }); - MatcherAssert.assertThat(e.getMessage(), CoreMatchers.containsString("Invalid key")); - } - - @Test - public void testInitForEncryption() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - CipherParameters params = new KeyParameter(new byte[16]); - Assertions.assertDoesNotThrow(() -> { - cipher.init(true, params); - }); - } - - @Test - public void testInitForEncryptionWithProvider() { - JceAesBlockCipher cipher = new JceAesBlockCipher(getSunJceProvider()); - CipherParameters params = new KeyParameter(new byte[16]); - Assertions.assertDoesNotThrow(() -> { - cipher.init(true, params); - }); - } - - @Test - public void testInitForDecryption() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - CipherParameters params = new KeyParameter(new byte[16]); - Assertions.assertDoesNotThrow(() -> { - cipher.init(false, params); - }); - } - - @Test - public void testInitForDecryptionWithProvider() { - JceAesBlockCipher cipher = new JceAesBlockCipher(getSunJceProvider()); - CipherParameters params = new KeyParameter(new byte[16]); - Assertions.assertDoesNotThrow(() -> { - cipher.init(false, params); - }); - } - - private Provider getSunJceProvider() { - Provider provider = Security.getProvider("SunJCE"); - Assertions.assertNotNull(provider); - return provider; - } - - @Test - public void testGetAlgorithmName() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - Assertions.assertEquals("AES", cipher.getAlgorithmName()); - } - - @Test - public void testGetBlockSize() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - Assertions.assertEquals(16, cipher.getBlockSize()); - } - - @Test - public void testProcessBlockWithUninitializedCipher() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - Assertions.assertThrows(IllegalStateException.class, () -> { - cipher.processBlock(new byte[16], 0, new byte[16], 0); - }); - } - - @Test - public void testProcessBlockWithInsufficientInput() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - cipher.init(true, new KeyParameter(new byte[16])); - DataLengthException e = Assertions.assertThrows(DataLengthException.class, () -> { - cipher.processBlock(new byte[16], 1, new byte[16], 0); - }); - MatcherAssert.assertThat(e.getMessage(), CoreMatchers.containsString("Insufficient data in 'in'")); - } - - @Test - public void testProcessBlockWithInsufficientOutput() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - cipher.init(true, new KeyParameter(new byte[16])); - DataLengthException e = Assertions.assertThrows(DataLengthException.class, () -> { - cipher.processBlock(new byte[16], 0, new byte[16], 1); - }); - MatcherAssert.assertThat(e.getMessage(), CoreMatchers.containsString("Insufficient space in 'out'")); - } - - @Test - public void testProcessBlock() { - testProcessBlock(new JceAesBlockCipher()); - } - - @Test - public void testProcessBlockWithProvider() { - testProcessBlock(new JceAesBlockCipher(getSunJceProvider())); - } - - private void testProcessBlock(JceAesBlockCipher cipher) { - cipher.init(true, new KeyParameter(new byte[16])); - byte[] ciphertext = new byte[16]; - int encrypted = cipher.processBlock(new byte[20], 0, ciphertext, 0); - Assertions.assertEquals(16, encrypted); - - cipher.init(false, new KeyParameter(new byte[16])); - byte[] cleartext = new byte[16]; - int decrypted = cipher.processBlock(ciphertext, 0, cleartext, 0); - Assertions.assertEquals(16, decrypted); - Assertions.assertArrayEquals(new byte[16], cleartext); - } - - @Test - public void testResetBeforeInitDoesNotThrowExceptions() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - Assertions.assertDoesNotThrow(cipher::reset); - } - - @Test - public void testResetAfterInitDoesNotThrowExceptions() { - JceAesBlockCipher cipher = new JceAesBlockCipher(); - cipher.init(true, new KeyParameter(new byte[16])); - Assertions.assertDoesNotThrow(cipher::reset); - } - -} diff --git a/src/test/java/org/cryptomator/siv/SivModeBenchmark.java b/src/test/java/org/cryptomator/siv/SivModeBenchmark.java index 8d7e610..f200024 100644 --- a/src/test/java/org/cryptomator/siv/SivModeBenchmark.java +++ b/src/test/java/org/cryptomator/siv/SivModeBenchmark.java @@ -1,17 +1,5 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - ******************************************************************************/ package org.cryptomator.siv; -import org.bouncycastle.crypto.BlockCipher; -import org.bouncycastle.crypto.engines.AESFastEngine; -import org.bouncycastle.crypto.engines.AESLightEngine; -import org.cryptomator.siv.SivMode.BlockCipherFactory; import org.junit.jupiter.api.Assertions; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; @@ -47,22 +35,6 @@ public class SivModeBenchmark { private final byte[] associatedData = new byte[100]; private final SivMode jceSivMode = new SivMode(); - private final SivMode bcFastSivMode = new SivMode(new BlockCipherFactory() { - - @Override - public BlockCipher create() { - return new AESFastEngine(); - } - - }); - private final SivMode bcLightSivMode = new SivMode(new BlockCipherFactory() { - - @Override - public BlockCipher create() { - return new AESLightEngine(); - } - - }); @Setup(Level.Trial) public void shuffleData() { @@ -82,22 +54,4 @@ public void benchmarkJce(Blackhole bh) throws UnauthenticCiphertextException, Il bh.consume(decrypted); } - @Benchmark - public void benchmarkBcFast(Blackhole bh) throws UnauthenticCiphertextException, IllegalBlockSizeException { - byte[] encrypted = bcFastSivMode.encrypt(encKey, macKey, cleartextData, associatedData); - byte[] decrypted = bcFastSivMode.decrypt(encKey, macKey, encrypted, associatedData); - Assertions.assertArrayEquals(cleartextData, decrypted); - bh.consume(encrypted); - bh.consume(decrypted); - } - - @Benchmark - public void benchmarkBcLight(Blackhole bh) throws UnauthenticCiphertextException, IllegalBlockSizeException { - byte[] encrypted = bcLightSivMode.encrypt(encKey, macKey, cleartextData, associatedData); - byte[] decrypted = bcLightSivMode.decrypt(encKey, macKey, encrypted, associatedData); - Assertions.assertArrayEquals(cleartextData, decrypted); - bh.consume(encrypted); - bh.consume(decrypted); - } - } diff --git a/src/test/java/org/cryptomator/siv/SivModeTest.java b/src/test/java/org/cryptomator/siv/SivModeTest.java index 7002df3..78437e4 100644 --- a/src/test/java/org/cryptomator/siv/SivModeTest.java +++ b/src/test/java/org/cryptomator/siv/SivModeTest.java @@ -8,11 +8,6 @@ * Sebastian Stenzel - initial API and implementation ******************************************************************************/ -import org.bouncycastle.crypto.engines.AESLightEngine; -import org.bouncycastle.crypto.engines.DESEngine; -import org.cryptomator.siv.SivMode.BlockCipherFactory; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DynamicContainer; import org.junit.jupiter.api.DynamicTest; @@ -79,25 +74,6 @@ public void testEncryptWithInvalidKey3() { }); } - @Test - public void testInvalidCipher1() { - BlockCipherFactory factory = () -> null; - - Assertions.assertThrows(NullPointerException.class, () -> { - new SivMode(factory); - }); - } - - @Test - public void testInvalidCipher2() { - BlockCipherFactory factory = DESEngine::new; // wrong block size - - IllegalArgumentException e = Assertions.assertThrows(IllegalArgumentException.class, () -> { - new SivMode(factory); - }); - MatcherAssert.assertThat(e.getMessage(), CoreMatchers.containsString("cipherFactory must create BlockCipher objects with a 16-byte block size")); - } - @Test public void testDecryptWithInvalidKey1() { SecretKey key1 = Mockito.mock(SecretKey.class); @@ -194,9 +170,6 @@ public void testComputeCtr1() { final byte[] sunJceResult = new SivMode(getSunJceProvider()).computeCtr(new byte[16], ctrKey, ctr); Assertions.assertArrayEquals(expected, sunJceResult); - - final byte[] bcResult = new SivMode(AESLightEngine::new).computeCtr(new byte[16], ctrKey, ctr); - Assertions.assertArrayEquals(expected, bcResult); } // CTR-AES https://tools.ietf.org/html/rfc5297#appendix-A.2 From c907ae7599197f781a1e4321a49313aac1d0b7dc Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 25 Oct 2025 09:17:32 +0200 Subject: [PATCH 02/25] performance optimizations --- src/main/java/org/cryptomator/siv/CMac.java | 40 +++++++++++++------ .../java/org/cryptomator/siv/SivMode.java | 7 +++- .../org/cryptomator/siv/SivModeBenchmark.java | 1 - 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java index 20493f0..41cbd15 100644 --- a/src/main/java/org/cryptomator/siv/CMac.java +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -5,6 +5,7 @@ import javax.crypto.IllegalBlockSizeException; import javax.crypto.MacSpi; import javax.crypto.NoSuchPaddingException; +import javax.crypto.ShortBufferException; import javax.crypto.spec.SecretKeySpec; import java.security.InvalidKeyException; import java.security.Key; @@ -23,14 +24,22 @@ class CMac extends MacSpi { private static final String AES_ALGORITHM = "AES"; private static final String AES_ECB_NO_PADDING = "AES/ECB/NoPadding"; + private static final ThreadLocal AES = ThreadLocals.withInitial(() -> { + try { + return Cipher.getInstance(AES_ECB_NO_PADDING); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new AssertionError("Every implementation of the Java platform is required to support [...] AES/ECB/NoPadding", e); + } + }); + // MAC keys: private Cipher cipher; private byte[] k1; private byte[] k2; // MAC state: + private final byte[] buffer = new byte[BLOCK_SIZE]; private int bufferPos = 0; - private byte[] buffer = new byte[BLOCK_SIZE]; private byte[] x = new byte[BLOCK_SIZE]; // X := const_Zero; private byte[] y = new byte[BLOCK_SIZE]; private int msgLen = 0; @@ -42,11 +51,12 @@ protected int engineGetMacLength() { @Override protected void engineInit(Key key, AlgorithmParameterSpec params) throws InvalidKeyException { - try { - this.cipher = Cipher.getInstance(AES_ECB_NO_PADDING); - } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - throw new AssertionError("Every implementation of the Java platform is required to support [...] AES/ECB/NoPadding", e); - } + this.cipher = AES.get(); +// try { +// this.cipher = Cipher.getInstance(AES_ECB_NO_PADDING); +// } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { +// throw new AssertionError("Every implementation of the Java platform is required to support [...] AES/ECB/NoPadding", e); +// } cipher.init(Cipher.ENCRYPT_MODE, key); // init subkeys K1 and K2 @@ -54,7 +64,7 @@ protected void engineInit(Key key, AlgorithmParameterSpec params) throws Invalid byte[] L = new byte[BLOCK_SIZE]; try { // L = AES_encrypt(K, const_Zero) - L = encryptBlock(cipher, L); + encryptBlock(cipher, L, L); this.k1 = SivMode.dbl(L); this.k2 = SivMode.dbl(k1); } finally { @@ -91,8 +101,8 @@ protected void engineUpdate(byte[] input, int offset, int len) { // https://www.rfc-editor.org/rfc/rfc4493.html#section-2.4 Step 6 private void processBlock() { - y = SivMode.xor(x, buffer); // Y := X XOR M_i; - x = encryptBlock(cipher, y); // X := AES-128(K,Y); + SivMode.xor(x, buffer, y); // Y := X XOR M_i; + encryptBlock(cipher, y, x); // X := AES-128(K,Y); bufferPos = 0; } @@ -121,9 +131,11 @@ protected byte[] engineDoFinal() { } // Step 7: - y = SivMode.xor(m_last, x); // Y := M_last XOR X; + SivMode.xor(m_last, x, y); // Y := M_last XOR X; try { - return encryptBlock(cipher, y); // T := AES-128(K,Y); + byte[] t = new byte[BLOCK_SIZE]; + encryptBlock(cipher, y, t); // T := AES-128(K,Y); + return t; } finally { engineReset(); } @@ -139,13 +151,15 @@ protected void engineReset() { } // TODO make instance method, remove cipher param? - private static byte[] encryptBlock(Cipher cipher, byte[] block) { + private static void encryptBlock(Cipher cipher, byte[] block, byte[] output) { try { - return cipher.doFinal(block); + cipher.doFinal(block, 0, BLOCK_SIZE, output); } catch (IllegalBlockSizeException e) { throw new IllegalArgumentException(e); } catch (BadPaddingException e) { throw new AssertionError("Not in decrypt mode", e); + } catch (ShortBufferException e) { + throw new IllegalArgumentException("Output buffer too short", e); } } diff --git a/src/main/java/org/cryptomator/siv/SivMode.java b/src/main/java/org/cryptomator/siv/SivMode.java index ae84dc9..8500bc1 100644 --- a/src/main/java/org/cryptomator/siv/SivMode.java +++ b/src/main/java/org/cryptomator/siv/SivMode.java @@ -318,10 +318,15 @@ static byte[] dbl(byte[] in) { static byte[] xor(byte[] in1, byte[] in2) { assert in1.length <= in2.length : "Length of first input must be <= length of second input."; final byte[] result = new byte[in1.length]; + xor(in1, in2, result); + return result; + } + + static void xor(byte[] in1, byte[] in2, byte[] result) { + assert result.length <= in1.length && result.length <= in2.length : "All inputs must have the same length."; for (int i = 0; i < result.length; i++) { result[i] = (byte) (in1[i] ^ in2[i]); } - return result; } @VisibleForTesting diff --git a/src/test/java/org/cryptomator/siv/SivModeBenchmark.java b/src/test/java/org/cryptomator/siv/SivModeBenchmark.java index f200024..818aae2 100644 --- a/src/test/java/org/cryptomator/siv/SivModeBenchmark.java +++ b/src/test/java/org/cryptomator/siv/SivModeBenchmark.java @@ -20,7 +20,6 @@ /** * Needs to be compiled via maven as the JMH annotation processor needs to do stuff... */ -@SuppressWarnings("deprecation") @State(Scope.Thread) @Warmup(iterations = 3, time = 300, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 2, time = 500, timeUnit = TimeUnit.MILLISECONDS) From 20a705fc81b846edac7bd4238a762db7f05d82cd Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 25 Oct 2025 23:20:00 +0200 Subject: [PATCH 03/25] clean up legacy code, simplify API --- src/main/java/org/cryptomator/siv/CMac.java | 24 +- .../cryptomator/siv/JceAesCtrComputer.java | 49 -- .../java/org/cryptomator/siv/SivMode.java | 212 ++---- .../org/cryptomator/siv/ThreadLocals.java | 24 - .../org/cryptomator/siv/package-info.java | 4 +- .../org.cryptomator.siv/ThreadLocals.java | 14 - .../cryptomator/siv/EncryptionTestCase.java | 7 + .../siv/JceAesCtrComputerTest.java | 60 -- .../org/cryptomator/siv/SivModeBenchmark.java | 13 +- .../java/org/cryptomator/siv/SivModeTest.java | 678 +++++++----------- 10 files changed, 331 insertions(+), 754 deletions(-) delete mode 100644 src/main/java/org/cryptomator/siv/JceAesCtrComputer.java delete mode 100644 src/main/java/org/cryptomator/siv/ThreadLocals.java delete mode 100644 src/main/java9/org.cryptomator.siv/ThreadLocals.java delete mode 100644 src/test/java/org/cryptomator/siv/JceAesCtrComputerTest.java diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java index 41cbd15..64ecb24 100644 --- a/src/main/java/org/cryptomator/siv/CMac.java +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -24,14 +24,6 @@ class CMac extends MacSpi { private static final String AES_ALGORITHM = "AES"; private static final String AES_ECB_NO_PADDING = "AES/ECB/NoPadding"; - private static final ThreadLocal AES = ThreadLocals.withInitial(() -> { - try { - return Cipher.getInstance(AES_ECB_NO_PADDING); - } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - throw new AssertionError("Every implementation of the Java platform is required to support [...] AES/ECB/NoPadding", e); - } - }); - // MAC keys: private Cipher cipher; private byte[] k1; @@ -40,8 +32,8 @@ class CMac extends MacSpi { // MAC state: private final byte[] buffer = new byte[BLOCK_SIZE]; private int bufferPos = 0; - private byte[] x = new byte[BLOCK_SIZE]; // X := const_Zero; - private byte[] y = new byte[BLOCK_SIZE]; + private final byte[] x = new byte[BLOCK_SIZE]; // X := const_Zero; + private final byte[] y = new byte[BLOCK_SIZE]; private int msgLen = 0; @Override @@ -51,12 +43,11 @@ protected int engineGetMacLength() { @Override protected void engineInit(Key key, AlgorithmParameterSpec params) throws InvalidKeyException { - this.cipher = AES.get(); -// try { -// this.cipher = Cipher.getInstance(AES_ECB_NO_PADDING); -// } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { -// throw new AssertionError("Every implementation of the Java platform is required to support [...] AES/ECB/NoPadding", e); -// } + try { + this.cipher = Cipher.getInstance(AES_ECB_NO_PADDING); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new AssertionError("Every implementation of the Java platform is required to support [...] AES/ECB/NoPadding", e); + } cipher.init(Cipher.ENCRYPT_MODE, key); // init subkeys K1 and K2 @@ -150,7 +141,6 @@ protected void engineReset() { Arrays.fill(y, (byte) 0); } - // TODO make instance method, remove cipher param? private static void encryptBlock(Cipher cipher, byte[] block, byte[] output) { try { cipher.doFinal(block, 0, BLOCK_SIZE, output); diff --git a/src/main/java/org/cryptomator/siv/JceAesCtrComputer.java b/src/main/java/org/cryptomator/siv/JceAesCtrComputer.java deleted file mode 100644 index 4c6fa3b..0000000 --- a/src/main/java/org/cryptomator/siv/JceAesCtrComputer.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.cryptomator.siv; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.Provider; - -/** - * Performs CTR Mode computations facilitating a cipher returned by JCE's Cipher.getInstance("AES/CTR/NoPadding"). - */ -final class JceAesCtrComputer implements SivMode.CtrComputer { - - private final ThreadLocal threadLocalCipher; - - public JceAesCtrComputer(final Provider jceSecurityProvider) { - this.threadLocalCipher = ThreadLocals.withInitial(() -> { - try { - if (jceSecurityProvider == null) { - return Cipher.getInstance("AES/CTR/NoPadding"); - } else { - return Cipher.getInstance("AES/CTR/NoPadding", jceSecurityProvider); - } - } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { - throw new IllegalStateException("AES/CTR/NoPadding not available on this platform.", e); - } - }); - } - - @Override - public byte[] computeCtr(byte[] input, byte[] key, final byte[] iv) { - try { - Cipher cipher = threadLocalCipher.get(); - cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv)); - return cipher.doFinal(input); - } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { - throw new IllegalArgumentException("Key or IV invalid."); - } catch (BadPaddingException e) { - throw new IllegalStateException("Cipher doesn't require padding.", e); - } catch (IllegalBlockSizeException e) { - throw new IllegalStateException("Block size irrelevant for stream ciphers.", e); - } - } -} diff --git a/src/main/java/org/cryptomator/siv/SivMode.java b/src/main/java/org/cryptomator/siv/SivMode.java index 8500bc1..0d2b4a3 100644 --- a/src/main/java/org/cryptomator/siv/SivMode.java +++ b/src/main/java/org/cryptomator/siv/SivMode.java @@ -2,9 +2,16 @@ import org.jetbrains.annotations.VisibleForTesting; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; -import java.security.Provider; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; /** @@ -15,84 +22,59 @@ public final class SivMode { private static final byte[] BYTES_ZERO = new byte[16]; private static final byte DOUBLING_CONST = (byte) 0x87; - private final CtrComputer ctrComputer; + private final SecretKey macKey; + private final SecretKey ctrKey; - /** - * Creates an AES-SIV instance using JCE's cipher implementation, which should normally be the best choice.
- */ - public SivMode() { - this( null); - } - - /** - * Creates an AES-SIV instance using a custom JCE's security provider
- * - * @param jceSecurityProvider to use to create the internal {@link javax.crypto.Cipher} instance - */ - public SivMode(final Provider jceSecurityProvider) { - this.ctrComputer = new JceAesCtrComputer(jceSecurityProvider); - } - - /** - * Performs CTR computations. - */ - @FunctionalInterface - interface CtrComputer { - byte[] computeCtr(byte[] input, byte[] key, final byte[] iv); - } + private final CMac cmac; + private final Cipher ctrCipher; /** - * Convenience method using a single 256, 384, or 512 bits key. This is just a wrapper for {@link #encrypt(byte[], byte[], byte[], byte[]...)}. + * Creates an AES-SIV instance using JCE's cipher implementation, which should normally be the best choice. * - * @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. + * @param key A 256, 384, or 512 bit key. The first half is used for nonce generation, the second half for encryption */ - public byte[] encrypt(SecretKey key, byte[] plaintext, byte[]... associatedData) { + public SivMode(byte[] key) { + if (key.length != 64 && key.length != 48 && key.length != 32) { + throw new IllegalArgumentException("Key length must be 256, 384, or 512 bits."); + } + final int subkeyLen = key.length / 2; + assert subkeyLen == 32 || subkeyLen == 24 || subkeyLen == 16; + final byte[] macKey = new byte[subkeyLen]; + final byte[] ctrKey = new byte[subkeyLen]; + System.arraycopy(key, 0, macKey, 0, macKey.length); // K1 = leftmost(K, len(K)/2); + System.arraycopy(key, macKey.length, ctrKey, 0, ctrKey.length); // K2 = rightmost(K, len(K)/2); + this.macKey = new SecretKeySpec(macKey, "AES"); + this.ctrKey = new SecretKeySpec(ctrKey, "AES"); + this.cmac = new CMac(); try { - return deriveSubkeysAndThen(this::encrypt, key, plaintext, associatedData); - } catch (UnauthenticCiphertextException | IllegalBlockSizeException e) { - throw new IllegalStateException("Exceptions only expected during decryption", e); + cmac.engineInit(this.macKey, null); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException(e); } - } - /** - * 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 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. - * @throws IllegalArgumentException if keys are invalid or {@link SecretKey#getEncoded()} is not supported. - */ - public byte[] encrypt(SecretKey ctrKey, SecretKey macKey, byte[] plaintext, byte[]... associatedData) { try { - return getEncodedAndThen(this::encrypt, ctrKey, macKey, plaintext, associatedData); - } catch (UnauthenticCiphertextException | IllegalBlockSizeException e) { - throw new IllegalStateException("Exceptions only expected during decryption", e); + this.ctrCipher = Cipher.getInstance("AES/CTR/NoPadding"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException("AES/CTR/NoPadding not available on this platform.", e); } } /** * 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 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. - * @throws IllegalArgumentException if the either of the two keys is of invalid length. + * @throws IllegalArgumentException if either of the two keys is of invalid length. */ - public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... associatedData) { + public byte[] encrypt(byte[] plaintext, byte[]... associatedData) { // Check if plaintext length will cause overflows if (plaintext.length > (Integer.MAX_VALUE - 16)) { throw new IllegalArgumentException("Plaintext is too long"); } - final byte[] iv = s2v(macKey, plaintext, associatedData); - final byte[] ciphertext = computeCtr(plaintext, ctrKey, iv); + final byte[] iv = s2v(plaintext, associatedData); + final byte[] ciphertext = computeCtr(plaintext, iv); // concat IV + ciphertext: final byte[] result = new byte[iv.length + ciphertext.length]; @@ -101,42 +83,9 @@ 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 { - return deriveSubkeysAndThen(this::decrypt, key, ciphertext, associatedData); - } - - /** - * 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 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. - * @throws IllegalArgumentException If keys are invalid or {@link SecretKey#getEncoded()} is not supported. - * @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 ctrKey, SecretKey macKey, byte[] ciphertext, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException { - return getEncodedAndThen(this::decrypt, ctrKey, macKey, ciphertext, associatedData); - } - /** * 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 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. @@ -144,15 +93,15 @@ public byte[] decrypt(SecretKey ctrKey, SecretKey macKey, byte[] ciphertext, byt * @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(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException { + public byte[] decrypt(byte[] ciphertext, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException { if (ciphertext.length < 16) { throw new IllegalBlockSizeException("Input length must be greater than or equal 16."); } final byte[] iv = Arrays.copyOf(ciphertext, 16); final byte[] actualCiphertext = Arrays.copyOfRange(ciphertext, 16, ciphertext.length); - final byte[] plaintext = computeCtr(actualCiphertext, ctrKey, iv); - final byte[] control = s2v(macKey, plaintext, associatedData); + final byte[] plaintext = computeCtr(actualCiphertext, iv); + final byte[] control = s2v(plaintext, associatedData); // time-constant comparison (taken from MessageDigest.isEqual in JDK8) assert iv.length == control.length; @@ -168,82 +117,27 @@ public byte[] decrypt(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]... } } - - /** - * Either {@link #encrypt(byte[], byte[], byte[], byte[]...)} or {@link #decrypt(byte[], byte[], byte[], byte[]...)}. - */ - @FunctionalInterface - private interface EncryptOrDecrypt { - byte[] compute(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException; - } - - /** - * Splits the key into two subkeys and then encrypts or decrypts the data. - * @param encryptOrDecrypt Either {@link #encrypt(byte[], byte[], byte[], byte[]...)} or {@link #decrypt(byte[], byte[], byte[], byte[]...)} - * @param key The combined key, with the leftmost half being the S2V key and the rightmost half being the CTR key - * @param data The to-be-encrypted plaintext or the to-be-decrypted ciphertext - * @param associatedData Optional associated data - * @return result of the encryptOrDecrypt function - * @throws UnauthenticCiphertextException If the authentication failed, e.g. because ciphertext and/or associatedData are corrupted (only during decryption). - * @throws IllegalBlockSizeException If the provided ciphertext is of invalid length (only during decryption). - */ - private byte[] deriveSubkeysAndThen(EncryptOrDecrypt encryptOrDecrypt, SecretKey key, byte[] data, 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 encryptOrDecrypt.compute(ctrKey, macKey, data, associatedData); - } finally { - Arrays.fill(macKey, (byte) 0); - Arrays.fill(ctrKey, (byte) 0); - Arrays.fill(keyBytes, (byte) 0); - } - } - - /** - * Encrypts or decrypts data using the given keys. - * @param encryptOrDecrypt Either {@link #encrypt(byte[], byte[], byte[], byte[]...)} or {@link #decrypt(byte[], byte[], byte[], byte[]...)} - * @param ctrKey The part of the key used for the CTR computation - * @param macKey The part of the key used for the S2V computation - * @param data The to-be-encrypted plaintext or the to-be-decrypted ciphertext - * @param associatedData Optional associated data - * @return result of the encryptOrDecrypt function - * @throws UnauthenticCiphertextException If the authentication failed, e.g. because ciphertext and/or associatedData are corrupted (only during decryption). - * @throws IllegalBlockSizeException If the provided ciphertext is of invalid length (only during decryption). - */ - private byte[] getEncodedAndThen(EncryptOrDecrypt encryptOrDecrypt, SecretKey ctrKey, SecretKey macKey, byte[] data, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException { - final byte[] ctrKeyBytes = ctrKey.getEncoded(); - final byte[] macKeyBytes = macKey.getEncoded(); - if (ctrKeyBytes == null || macKeyBytes == null) { - throw new IllegalArgumentException("Can't get bytes of given key."); - } - try { - return encryptOrDecrypt.compute(ctrKeyBytes, macKeyBytes, data, associatedData); - } finally { - Arrays.fill(ctrKeyBytes, (byte) 0); - Arrays.fill(macKeyBytes, (byte) 0); - } - } - @VisibleForTesting - byte[] computeCtr(byte[] input, byte[] key, final byte[] iv) { + byte[] computeCtr(byte[] input, final byte[] iv) { // clear out the 31st and 63rd (rightmost) bit: final byte[] adjustedIv = Arrays.copyOf(iv, 16); adjustedIv[8] = (byte) (adjustedIv[8] & 0x7F); adjustedIv[12] = (byte) (adjustedIv[12] & 0x7F); - return ctrComputer.computeCtr(input, key, adjustedIv); + try { + ctrCipher.init(Cipher.ENCRYPT_MODE, ctrKey, new IvParameterSpec(adjustedIv)); + return ctrCipher.doFinal(input); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalArgumentException("Key or IV invalid."); + } catch (BadPaddingException e) { + throw new IllegalStateException("Cipher doesn't require padding.", e); + } catch (IllegalBlockSizeException e) { + throw new IllegalStateException("Block size irrelevant for stream ciphers.", e); + } } @VisibleForTesting - byte[] s2v(byte[] macKey, byte[] plaintext, byte[]... associatedData) throws IllegalArgumentException { + byte[] s2v(byte[] plaintext, byte[]... associatedData) throws IllegalArgumentException { // Maximum permitted AD length is the block size in bits - 2 if (associatedData.length > 126) { // SIV mode cannot be used safely with this many AD fields @@ -255,8 +149,6 @@ byte[] s2v(byte[] macKey, byte[] plaintext, byte[]... associatedData) throws Ill // Since this method is invoked only by encrypt/decrypt, we always have a plaintext. // Thus n > 0 - CMac cmac = CMac.create(macKey); - byte[] d = mac(cmac, BYTES_ZERO); for (byte[] s : associatedData) { diff --git a/src/main/java/org/cryptomator/siv/ThreadLocals.java b/src/main/java/org/cryptomator/siv/ThreadLocals.java deleted file mode 100644 index 7eae70f..0000000 --- a/src/main/java/org/cryptomator/siv/ThreadLocals.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.siv; - -import java.util.function.Supplier; - -/** - * Shim for Android 7.x - * @see Issue 17 - */ -class ThreadLocals { - - private ThreadLocals() { - } - - static ThreadLocal withInitial(Supplier supplier) { - // ThreadLocal.withInitial is unavailable on Android 7.x - return new ThreadLocal() { - @Override - protected S initialValue() { - return supplier.get(); - } - }; - } - -} diff --git a/src/main/java/org/cryptomator/siv/package-info.java b/src/main/java/org/cryptomator/siv/package-info.java index f3bbc1c..c598bd1 100644 --- a/src/main/java/org/cryptomator/siv/package-info.java +++ b/src/main/java/org/cryptomator/siv/package-info.java @@ -2,7 +2,7 @@ * Java implementation of RFC 5297 SIV Authenticated Encryption. *

* Use an instance of the {@link org.cryptomator.siv.SivMode} class to - * {@link org.cryptomator.siv.SivMode#encrypt(javax.crypto.SecretKey, javax.crypto.SecretKey, byte[], byte[]...) encrypt} or - * {@link org.cryptomator.siv.SivMode#decrypt(javax.crypto.SecretKey, javax.crypto.SecretKey, byte[], byte[]...) decrypt} data. + * {@link org.cryptomator.siv.SivMode#encrypt(byte[], byte[]...) encrypt} or + * {@link org.cryptomator.siv.SivMode#decrypt(byte[], byte[]...) decrypt} data. */ package org.cryptomator.siv; \ No newline at end of file diff --git a/src/main/java9/org.cryptomator.siv/ThreadLocals.java b/src/main/java9/org.cryptomator.siv/ThreadLocals.java deleted file mode 100644 index b7dc973..0000000 --- a/src/main/java9/org.cryptomator.siv/ThreadLocals.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.cryptomator.siv; - -import java.util.function.Supplier; - -class ThreadLocals { - - private ThreadLocals() { - } - - static ThreadLocal withInitial(Supplier supplier) { - return ThreadLocal.withInitial(supplier); - } - -} diff --git a/src/test/java/org/cryptomator/siv/EncryptionTestCase.java b/src/test/java/org/cryptomator/siv/EncryptionTestCase.java index a86591e..8dfba8e 100644 --- a/src/test/java/org/cryptomator/siv/EncryptionTestCase.java +++ b/src/test/java/org/cryptomator/siv/EncryptionTestCase.java @@ -50,6 +50,13 @@ public int getTestCaseNumber() { return testCaseNumber; } + public byte[] getKey() { + byte[] key = new byte[macKey.length + ctrKey.length]; + System.arraycopy(macKey, 0, key, 0, macKey.length); + System.arraycopy(ctrKey, 0, key, macKey.length, ctrKey.length); + return key; + } + public byte[] getCtrKey() { return Arrays.copyOf(ctrKey, ctrKey.length); } diff --git a/src/test/java/org/cryptomator/siv/JceAesCtrComputerTest.java b/src/test/java/org/cryptomator/siv/JceAesCtrComputerTest.java deleted file mode 100644 index c2bca3a..0000000 --- a/src/test/java/org/cryptomator/siv/JceAesCtrComputerTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.cryptomator.siv; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class JceAesCtrComputerTest { - - // CTR-AES https://tools.ietf.org/html/rfc5297#appendix-A.1 - @Test - public void testComputeCtr1() { - byte[] ctrKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, // - (byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, // - (byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, // - (byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff}; - - byte[] ctr = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // - (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // - (byte) 0x15, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // - (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93}; - - byte[] expected = {(byte) 0x51, (byte) 0xe2, (byte) 0x18, (byte) 0xd2, // - (byte) 0xc5, (byte) 0xa2, (byte) 0xab, (byte) 0x8c, // - (byte) 0x43, (byte) 0x45, (byte) 0xc4, (byte) 0xa6, // - (byte) 0x23, (byte) 0xb2, (byte) 0xf0, (byte) 0x8f}; - - byte[] result = new JceAesCtrComputer(null).computeCtr(new byte[16], ctrKey, ctr); - Assertions.assertArrayEquals(expected, result); - } - - // CTR-AES https://tools.ietf.org/html/rfc5297#appendix-A.2 - @Test - public void testComputeCtr2() { - final byte[] ctrKey = {(byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, // - (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, // - (byte) 0x48, (byte) 0x49, (byte) 0x4a, (byte) 0x4b, // - (byte) 0x4c, (byte) 0x4d, (byte) 0x4e, (byte) 0x4f}; - - final byte[] ctr = {(byte) 0x7b, (byte) 0xdb, (byte) 0x6e, (byte) 0x3b, // - (byte) 0x43, (byte) 0x26, (byte) 0x67, (byte) 0xeb, // - (byte) 0x06, (byte) 0xf4, (byte) 0xd1, (byte) 0x4b, // - (byte) 0x7f, (byte) 0x2f, (byte) 0xbd, (byte) 0x0f}; - - final byte[] expected = {(byte) 0xbf, (byte) 0xf8, (byte) 0x66, (byte) 0x5c, // - (byte) 0xfd, (byte) 0xd7, (byte) 0x33, (byte) 0x63, // - (byte) 0x55, (byte) 0x0f, (byte) 0x74, (byte) 0x00, // - (byte) 0xe8, (byte) 0xf9, (byte) 0xd3, (byte) 0x76, // - (byte) 0xb2, (byte) 0xc9, (byte) 0x08, (byte) 0x8e, // - (byte) 0x71, (byte) 0x3b, (byte) 0x86, (byte) 0x17, // - (byte) 0xd8, (byte) 0x83, (byte) 0x92, (byte) 0x26, // - (byte) 0xd9, (byte) 0xf8, (byte) 0x81, (byte) 0x59, // - (byte) 0x9e, (byte) 0x44, (byte) 0xd8, (byte) 0x27, // - (byte) 0x23, (byte) 0x49, (byte) 0x49, (byte) 0xbc, // - (byte) 0x1b, (byte) 0x12, (byte) 0x34, (byte) 0x8e, // - (byte) 0xbc, (byte) 0x19, (byte) 0x5e, (byte) 0xc7}; - - byte[] result = new JceAesCtrComputer(null).computeCtr(new byte[48], ctrKey, ctr); - Assertions.assertArrayEquals(expected, result); - } - -} diff --git a/src/test/java/org/cryptomator/siv/SivModeBenchmark.java b/src/test/java/org/cryptomator/siv/SivModeBenchmark.java index 818aae2..7cf27f4 100644 --- a/src/test/java/org/cryptomator/siv/SivModeBenchmark.java +++ b/src/test/java/org/cryptomator/siv/SivModeBenchmark.java @@ -28,26 +28,25 @@ public class SivModeBenchmark { private int run; - private final byte[] encKey = new byte[16]; - private final byte[] macKey = new byte[16]; + private final byte[] key = new byte[32]; private final byte[] cleartextData = new byte[1000]; private final byte[] associatedData = new byte[100]; - private final SivMode jceSivMode = new SivMode(); + private SivMode siv; @Setup(Level.Trial) public void shuffleData() { run++; - Arrays.fill(encKey, (byte) (run & 0xFF)); - Arrays.fill(macKey, (byte) (run & 0xFF)); + Arrays.fill(key, (byte) (run & 0xFF)); + siv = new SivMode(key); Arrays.fill(cleartextData, (byte) (run & 0xFF)); Arrays.fill(associatedData, (byte) (run & 0xFF)); } @Benchmark public void benchmarkJce(Blackhole bh) throws UnauthenticCiphertextException, IllegalBlockSizeException { - byte[] encrypted = jceSivMode.encrypt(encKey, macKey, cleartextData, associatedData); - byte[] decrypted = jceSivMode.decrypt(encKey, macKey, encrypted, associatedData); + byte[] encrypted = siv.encrypt(cleartextData, associatedData); + byte[] decrypted = siv.decrypt(encrypted, associatedData); Assertions.assertArrayEquals(cleartextData, decrypted); bh.consume(encrypted); bh.consume(decrypted); diff --git a/src/test/java/org/cryptomator/siv/SivModeTest.java b/src/test/java/org/cryptomator/siv/SivModeTest.java index 78437e4..2177b32 100644 --- a/src/test/java/org/cryptomator/siv/SivModeTest.java +++ b/src/test/java/org/cryptomator/siv/SivModeTest.java @@ -1,25 +1,15 @@ package org.cryptomator.siv; -/******************************************************************************* - * Copyright (c) 2015 Sebastian Stenzel - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - ******************************************************************************/ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DynamicContainer; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; 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; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -27,8 +17,6 @@ import java.io.Reader; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; -import java.security.Provider; -import java.security.Security; import java.util.Arrays; import java.util.stream.Stream; @@ -37,258 +25,87 @@ */ public class SivModeTest { - @Test - public void testEncryptWithInvalidKey1() { - SecretKey key1 = Mockito.mock(SecretKey.class); - Mockito.when(key1.getEncoded()).thenReturn(null); - SecretKey key2 = Mockito.mock(SecretKey.class); - Mockito.when(key2.getEncoded()).thenReturn(new byte[16]); - - SivMode sivMode = new SivMode(); - Assertions.assertThrows(IllegalArgumentException.class, () -> { - sivMode.encrypt(key1, key2, new byte[10]); - }); - } - - @Test - public void testEncryptWithInvalidKey2() { - SecretKey key1 = Mockito.mock(SecretKey.class); - Mockito.when(key1.getEncoded()).thenReturn(new byte[16]); - SecretKey key2 = Mockito.mock(SecretKey.class); - Mockito.when(key2.getEncoded()).thenReturn(null); - - SivMode sivMode = new SivMode(); - Assertions.assertThrows(IllegalArgumentException.class, () -> { - sivMode.encrypt(key1, key2, new byte[10]); - }); - } - - @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 testDecryptWithInvalidKey1() { - SecretKey key1 = Mockito.mock(SecretKey.class); - Mockito.when(key1.getEncoded()).thenReturn(null); - SecretKey key2 = Mockito.mock(SecretKey.class); - Mockito.when(key2.getEncoded()).thenReturn(new byte[16]); - - SivMode sivMode = new SivMode(); - Assertions.assertThrows(IllegalArgumentException.class, () -> { - sivMode.decrypt(key1, key2, new byte[16]); - }); - } - - @Test - public void testDecryptWithInvalidKey2() { - SecretKey key1 = Mockito.mock(SecretKey.class); - Mockito.when(key1.getEncoded()).thenReturn(new byte[16]); - SecretKey key2 = Mockito.mock(SecretKey.class); - Mockito.when(key2.getEncoded()).thenReturn(null); - - SivMode sivMode = new SivMode(); - Assertions.assertThrows(IllegalArgumentException.class, () -> { - sivMode.decrypt(key1, key2, new byte[10]); - }); - } - - @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]; - final SecretKey ctrKey = new SecretKeySpec(dummyKey, "AES"); - final SecretKey macKey = new SecretKeySpec(dummyKey, "AES"); - - SivMode sivMode = new SivMode(); - Assertions.assertThrows(IllegalBlockSizeException.class, () -> { - sivMode.decrypt(ctrKey, macKey, new byte[10]); - }); - } - - @Test - public void testEncryptAssociatedDataLimit() { - final byte[] ctrKey = new byte[16]; - final byte[] macKey = new byte[16]; - final byte[] plaintext = new byte[30]; - - SivMode sivMode = new SivMode(); - Assertions.assertThrows(IllegalArgumentException.class, () -> { - sivMode.encrypt(ctrKey, macKey, plaintext, new byte[127][0]); - }); + @Nested + public class ParameterValidation { + + @ParameterizedTest + @ValueSource(ints = {0, 31, 33, 47, 49, 63, 65}) + public void testCreateWithInvalidKeyLength(int keylen) { + byte[] key = new byte[keylen]; + + Assertions.assertThrows(IllegalArgumentException.class, () -> new SivMode(key)); + } + + @Test + public void testDecryptWithInvalidBlockSize() { + final byte[] key = new byte[32]; + + SivMode sivMode = new SivMode(key); + Assertions.assertThrows(IllegalBlockSizeException.class, () -> { + sivMode.decrypt(new byte[10]); + }); + } + + @Test + public void testEncryptAssociatedDataLimit() { + final byte[] key = new byte[32]; + final byte[] plaintext = new byte[30]; + + SivMode sivMode = new SivMode(key); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + sivMode.encrypt(plaintext, new byte[127][0]); + }); + } + + @Test + public void testDecryptAssociatedDataLimit() { + final byte[] key = new byte[32]; + final byte[] plaintext = new byte[80]; + + SivMode sivMode = new SivMode(key); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + sivMode.decrypt(plaintext, new byte[127][0]); + }); + } } - @Test - public void testDecryptAssociatedDataLimit() { - final byte[] ctrKey = new byte[16]; - final byte[] macKey = new byte[16]; - final byte[] plaintext = new byte[80]; - - SivMode sivMode = new SivMode(); - Assertions.assertThrows(IllegalArgumentException.class, () -> { - sivMode.decrypt(ctrKey, macKey, plaintext, new byte[127][0]); - }); - } - - // CTR-AES https://tools.ietf.org/html/rfc5297#appendix-A.1 - @Test - public void testComputeCtr1() { - final byte[] ctrKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, // - (byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, // - (byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, // - (byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff}; - - final byte[] ctr = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // - (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // - (byte) 0x15, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // - (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93}; - - final byte[] expected = {(byte) 0x51, (byte) 0xe2, (byte) 0x18, (byte) 0xd2, // - (byte) 0xc5, (byte) 0xa2, (byte) 0xab, (byte) 0x8c, // - (byte) 0x43, (byte) 0x45, (byte) 0xc4, (byte) 0xa6, // - (byte) 0x23, (byte) 0xb2, (byte) 0xf0, (byte) 0x8f}; - - final byte[] result = new SivMode().computeCtr(new byte[16], ctrKey, ctr); - Assertions.assertArrayEquals(expected, result); - - final byte[] sunJceResult = new SivMode(getSunJceProvider()).computeCtr(new byte[16], ctrKey, ctr); - Assertions.assertArrayEquals(expected, sunJceResult); + @ParameterizedTest + @ValueSource(ints = {32, 48, 64}) + public void testEncryptionAndDecryption(int keylen) throws UnauthenticCiphertextException, IllegalBlockSizeException { + final byte[] key = new byte[keylen]; + final SivMode sivMode = new SivMode(key); + final byte[] cleartext = "hello world".getBytes(); + final byte[] ciphertext = sivMode.encrypt(cleartext); + final byte[] decrypted = sivMode.decrypt(ciphertext); + Assertions.assertArrayEquals(cleartext, decrypted); } - // CTR-AES https://tools.ietf.org/html/rfc5297#appendix-A.2 - @Test - public void testComputeCtr2() { - final byte[] ctrKey = {(byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, // - (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, // - (byte) 0x48, (byte) 0x49, (byte) 0x4a, (byte) 0x4b, // - (byte) 0x4c, (byte) 0x4d, (byte) 0x4e, (byte) 0x4f}; - - final byte[] ctr = {(byte) 0x7b, (byte) 0xdb, (byte) 0x6e, (byte) 0x3b, // - (byte) 0x43, (byte) 0x26, (byte) 0x67, (byte) 0xeb, // - (byte) 0x06, (byte) 0xf4, (byte) 0xd1, (byte) 0x4b, // - (byte) 0x7f, (byte) 0x2f, (byte) 0xbd, (byte) 0x0f}; - - final byte[] expected = {(byte) 0xbf, (byte) 0xf8, (byte) 0x66, (byte) 0x5c, // - (byte) 0xfd, (byte) 0xd7, (byte) 0x33, (byte) 0x63, // - (byte) 0x55, (byte) 0x0f, (byte) 0x74, (byte) 0x00, // - (byte) 0xe8, (byte) 0xf9, (byte) 0xd3, (byte) 0x76, // - (byte) 0xb2, (byte) 0xc9, (byte) 0x08, (byte) 0x8e, // - (byte) 0x71, (byte) 0x3b, (byte) 0x86, (byte) 0x17, // - (byte) 0xd8, (byte) 0x83, (byte) 0x92, (byte) 0x26, // - (byte) 0xd9, (byte) 0xf8, (byte) 0x81, (byte) 0x59, // - (byte) 0x9e, (byte) 0x44, (byte) 0xd8, (byte) 0x27, // - (byte) 0x23, (byte) 0x49, (byte) 0x49, (byte) 0xbc, // - (byte) 0x1b, (byte) 0x12, (byte) 0x34, (byte) 0x8e, // - (byte) 0xbc, (byte) 0x19, (byte) 0x5e, (byte) 0xc7}; - - final byte[] result = new SivMode().computeCtr(new byte[48], ctrKey, ctr); - Assertions.assertArrayEquals(expected, result); - } + // https://tools.ietf.org/html/rfc5297#appendix-A.1 + @Nested + public class RfcTestVector1 { - @Test - public void testS2v() { - final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, // + private final byte[] key = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, // (byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, // (byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, // - (byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0}; - - final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, // - (byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, // - (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, // - (byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, // - (byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, // - (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27}; + (byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0, // - final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, // - (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, // - (byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, // - (byte) 0xdd, (byte) 0xee}; - - final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // - (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // - (byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // - (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93}; - - final byte[] result = new SivMode().s2v(macKey, plaintext, ad); - Assertions.assertArrayEquals(expected, result); - - final byte[] resultProvider = new SivMode(getSunJceProvider()).s2v(macKey, plaintext, ad); - Assertions.assertArrayEquals(expected, resultProvider); - } - - @Test - public void testSivEncrypt() { - final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, // - (byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, // - (byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, // - (byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0}; - - final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, // + (byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, // (byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, // (byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, // (byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff}; - final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, // + private final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, // (byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, // (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, // (byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, // (byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, // (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27}; - final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, // + private final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, // (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, // (byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, // (byte) 0xdd, (byte) 0xee}; - final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // - (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // - (byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // - (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, // - (byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, // - (byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, // - (byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, // - (byte) 0xfe, (byte) 0x5c}; - - final byte[] result = new SivMode().encrypt(aesKey, macKey, plaintext, ad); - Assertions.assertArrayEquals(expected, result); - } - - @Test - public void testSivDecrypt() throws UnauthenticCiphertextException, IllegalBlockSizeException { - final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, // - (byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, // - (byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, // - (byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0}; - - final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, // - (byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, // - (byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, // - (byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff}; - - final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, // - (byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, // - (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, // - (byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, // - (byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, // - (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27}; - final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // (byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // @@ -298,83 +115,91 @@ public void testSivDecrypt() throws UnauthenticCiphertextException, IllegalBlock (byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, // (byte) 0xfe, (byte) 0x5c}; - final byte[] expected = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, // - (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, // - (byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, // - (byte) 0xdd, (byte) 0xee}; - - final byte[] result = new SivMode().decrypt(aesKey, macKey, ciphertext, ad); - Assertions.assertArrayEquals(expected, result); - } - - @Test - public void testSivDecryptWithInvalidKey() { - final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, // - (byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, // - (byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, // - (byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0}; - - final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, // - (byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, // - (byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, // - (byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0x00}; - - final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, // - (byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, // - (byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, // - (byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, // - (byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, // - (byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27}; - - final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // - (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // - (byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // - (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, // - (byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, // - (byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, // - (byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, // - (byte) 0xfe, (byte) 0x5c}; + // CTR-AES + @Test + public void testComputeCtr() { + final byte[] ctr = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // + (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // + (byte) 0x15, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // + (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93}; + + final byte[] expected = {(byte) 0x51, (byte) 0xe2, (byte) 0x18, (byte) 0xd2, // + (byte) 0xc5, (byte) 0xa2, (byte) 0xab, (byte) 0x8c, // + (byte) 0x43, (byte) 0x45, (byte) 0xc4, (byte) 0xa6, // + (byte) 0x23, (byte) 0xb2, (byte) 0xf0, (byte) 0x8f}; + + final byte[] result = new SivMode(key).computeCtr(new byte[16], ctr); + Assertions.assertArrayEquals(expected, result); + } + + @Test + public void testS2v() { + final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // + (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // + (byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // + (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93}; + + final byte[] result = new SivMode(key).s2v(plaintext, ad); + Assertions.assertArrayEquals(expected, result); + } + + @Test + public void testSivEncrypt() { + final byte[] result = new SivMode(key).encrypt(plaintext, ad); + Assertions.assertArrayEquals(ciphertext, result); + } + + @Test + public void testSivDecrypt() throws UnauthenticCiphertextException, IllegalBlockSizeException { + final byte[] result = new SivMode(key).decrypt(ciphertext, ad); + Assertions.assertArrayEquals(plaintext, result); + } + + @Test + public void testSivDecryptWithInvalidKey() { + final byte[] invalidKey = Arrays.copyOf(key, key.length); + invalidKey[invalidKey.length - 1] = 0x00; + + SivMode sivMode = new SivMode(invalidKey); + Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { + sivMode.decrypt(ciphertext, ad); + }); + } + + @Test + public void testSivDecryptWithInvalidCiphertext() { + final byte[] invalidCiphertext = Arrays.copyOf(ciphertext, ciphertext.length); + invalidCiphertext[invalidCiphertext.length - 1] = 0x00; + + SivMode sivMode = new SivMode(key); + Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { + sivMode.decrypt(invalidCiphertext); + }); + } + + @Test + public void testSivDecryptWithTruncatedCiphertext() { + final byte[] invalidCiphertext = Arrays.copyOf(ciphertext, 15); + + SivMode sivMode = new SivMode(key); + Assertions.assertThrows(IllegalBlockSizeException.class, () -> { + sivMode.decrypt(invalidCiphertext); + }); + } - SivMode sivMode = new SivMode(); - Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(aesKey, macKey, ciphertext, ad); - }); } - @Test - public void testSivDecryptWithInvalidCiphertext() { - final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, // - (byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, // - (byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, // - (byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0}; - - final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, // - (byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, // - (byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, // - (byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0x00}; - - final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, // - (byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, // - (byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // - (byte) 0x0a, (byte) 0x2e, (byte) 0xcc}; - - SivMode sivMode = new SivMode(); - Assertions.assertThrows(IllegalBlockSizeException.class, () -> { - sivMode.decrypt(aesKey, macKey, ciphertext); - }); - } + // https://tools.ietf.org/html/rfc5297#appendix-A.2 + @Nested + public class RfcTestVector2 { - /** - * https://tools.ietf.org/html/rfc5297#appendix-A.2 - */ - @Test - public void testNonceBasedAuthenticatedEncryption() { - final byte[] macKey = {(byte) 0x7f, (byte) 0x7e, (byte) 0x7d, (byte) 0x7c, // + private final byte[] key = { + (byte) 0x7f, (byte) 0x7e, (byte) 0x7d, (byte) 0x7c, // (byte) 0x7b, (byte) 0x7a, (byte) 0x79, (byte) 0x78, // (byte) 0x77, (byte) 0x76, (byte) 0x75, (byte) 0x74, // - (byte) 0x73, (byte) 0x72, (byte) 0x71, (byte) 0x70}; + (byte) 0x73, (byte) 0x72, (byte) 0x71, (byte) 0x70, // - final byte[] aesKey = {(byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, // + (byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, // (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, // (byte) 0x48, (byte) 0x49, (byte) 0x4a, (byte) 0x4b, // (byte) 0x4c, (byte) 0x4d, (byte) 0x4e, (byte) 0x4f}; @@ -412,9 +237,7 @@ public void testNonceBasedAuthenticatedEncryption() { (byte) 0x53, (byte) 0x49, (byte) 0x56, (byte) 0x2d, // (byte) 0x41, (byte) 0x45, (byte) 0x53}; - final byte[] result = new SivMode().encrypt(aesKey, macKey, plaintext, ad1, ad2, nonce); - - final byte[] expected = {(byte) 0x7b, (byte) 0xdb, (byte) 0x6e, (byte) 0x3b, // + final byte[] ciphertext = {(byte) 0x7b, (byte) 0xdb, (byte) 0x6e, (byte) 0x3b, // (byte) 0x43, (byte) 0x26, (byte) 0x67, (byte) 0xeb, // (byte) 0x06, (byte) 0xf4, (byte) 0xd1, (byte) 0x4b, // (byte) 0xff, (byte) 0x2f, (byte) 0xbd, (byte) 0x0f, // @@ -431,97 +254,112 @@ public void testNonceBasedAuthenticatedEncryption() { (byte) 0x48, (byte) 0x5b, (byte) 0x62, (byte) 0xa3, // (byte) 0xfd, (byte) 0x5c, (byte) 0x0d}; - Assertions.assertArrayEquals(expected, result); - } + @Test + public void testComputeCtr() { + final byte[] ctr = {(byte) 0x7b, (byte) 0xdb, (byte) 0x6e, (byte) 0x3b, // + (byte) 0x43, (byte) 0x26, (byte) 0x67, (byte) 0xeb, // + (byte) 0x06, (byte) 0xf4, (byte) 0xd1, (byte) 0x4b, // + (byte) 0x7f, (byte) 0x2f, (byte) 0xbd, (byte) 0x0f}; + + final byte[] expected = {(byte) 0xbf, (byte) 0xf8, (byte) 0x66, (byte) 0x5c, // + (byte) 0xfd, (byte) 0xd7, (byte) 0x33, (byte) 0x63, // + (byte) 0x55, (byte) 0x0f, (byte) 0x74, (byte) 0x00, // + (byte) 0xe8, (byte) 0xf9, (byte) 0xd3, (byte) 0x76, // + (byte) 0xb2, (byte) 0xc9, (byte) 0x08, (byte) 0x8e, // + (byte) 0x71, (byte) 0x3b, (byte) 0x86, (byte) 0x17, // + (byte) 0xd8, (byte) 0x83, (byte) 0x92, (byte) 0x26, // + (byte) 0xd9, (byte) 0xf8, (byte) 0x81, (byte) 0x59, // + (byte) 0x9e, (byte) 0x44, (byte) 0xd8, (byte) 0x27, // + (byte) 0x23, (byte) 0x49, (byte) 0x49, (byte) 0xbc, // + (byte) 0x1b, (byte) 0x12, (byte) 0x34, (byte) 0x8e, // + (byte) 0xbc, (byte) 0x19, (byte) 0x5e, (byte) 0xc7}; + + final byte[] result = new SivMode(key).computeCtr(new byte[48], ctr); + Assertions.assertArrayEquals(expected, result); + } + + @Test + public void testSivEncrypt() { + final byte[] result = new SivMode(key).encrypt(plaintext, ad1, ad2, nonce); + Assertions.assertArrayEquals(ciphertext, result); + } + + @Test + public void testSivDecrypt() throws UnauthenticCiphertextException, IllegalBlockSizeException { + final byte[] result = new SivMode(key).decrypt(ciphertext, ad1, ad2, nonce); + Assertions.assertArrayEquals(plaintext, result); + } - @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(); - final byte[] cleartext = "hello world".getBytes(); - final byte[] ciphertext = sivMode.encrypt(ctrKey, macKey, cleartext); - final byte[] decrypted = sivMode.decrypt(ctrKey, macKey, ciphertext); - 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]; - - SivMode.shiftLeft(new byte[] {(byte) 0x77, (byte) 0x3A, (byte) 0x87, (byte) 0x22}, output); - Assertions.assertArrayEquals(new byte[] {(byte) 0xEE, (byte) 0x75, (byte) 0x0E, (byte) 0x44}, output); - - SivMode.shiftLeft(new byte[] {(byte) 0x56, (byte) 0x12, (byte) 0x34, (byte) 0x99}, output); - Assertions.assertArrayEquals(new byte[] {(byte) 0xAC, (byte) 0x24, (byte) 0x69, (byte) 0x32}, output); - - SivMode.shiftLeft(new byte[] {(byte) 0xCF, (byte) 0xAB, (byte) 0xBA, (byte) 0x78}, output); - Assertions.assertArrayEquals(new byte[] {(byte) 0x9F, (byte) 0x57, (byte) 0x74, (byte) 0xF0}, output); + @Nested + public class HelperMethods { + + @Test + public void testShiftLeft() { + final byte[] output = new byte[4]; + + SivMode.shiftLeft(new byte[]{(byte) 0x77, (byte) 0x3A, (byte) 0x87, (byte) 0x22}, output); + Assertions.assertArrayEquals(new byte[]{(byte) 0xEE, (byte) 0x75, (byte) 0x0E, (byte) 0x44}, output); + + SivMode.shiftLeft(new byte[]{(byte) 0x56, (byte) 0x12, (byte) 0x34, (byte) 0x99}, output); + Assertions.assertArrayEquals(new byte[]{(byte) 0xAC, (byte) 0x24, (byte) 0x69, (byte) 0x32}, output); + + SivMode.shiftLeft(new byte[]{(byte) 0xCF, (byte) 0xAB, (byte) 0xBA, (byte) 0x78}, output); + Assertions.assertArrayEquals(new byte[]{(byte) 0x9F, (byte) 0x57, (byte) 0x74, (byte) 0xF0}, output); + + SivMode.shiftLeft(new byte[]{(byte) 0x89, (byte) 0x65, (byte) 0x43, (byte) 0x21}, output); + Assertions.assertArrayEquals(new byte[]{(byte) 0x12, (byte) 0xCA, (byte) 0x86, (byte) 0x42}, output); + } + + @Test + public void testDouble() { + Assertions.assertArrayEquals( + new byte[]{(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00,}, + SivMode.dbl(new byte[]{(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00,})); + + Assertions.assertArrayEquals( + new byte[]{(byte) 0x22, (byte) 0x44, (byte) 0x66, (byte) 0x88, (byte) 0xAA, (byte) 0xCC, (byte) 0xEF, (byte) 0x10, (byte) 0x22, (byte) 0x44, (byte) 0x66, (byte) 0x88, (byte) 0x22, (byte) 0x44, + (byte) 0x22, (byte) 0x44,}, + SivMode.dbl(new byte[]{(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x11, + (byte) 0x22, (byte) 0x11, (byte) 0x22,})); + + Assertions.assertArrayEquals( + new byte[]{(byte) 0x10, (byte) 0x88, (byte) 0x44, (byte) 0x23, (byte) 0x32, (byte) 0xEE, (byte) 0xAA, (byte) 0x66, (byte) 0x22, (byte) 0x66, (byte) 0xAA, (byte) 0xEE, (byte) 0x22, (byte) 0x44, + (byte) 0x89, (byte) 0x97,}, + SivMode.dbl(new byte[]{(byte) 0x88, (byte) 0x44, (byte) 0x22, (byte) 0x11, (byte) 0x99, (byte) 0x77, (byte) 0x55, (byte) 0x33, (byte) 0x11, (byte) 0x33, (byte) 0x55, (byte) 0x77, (byte) 0x11, + (byte) 0x22, (byte) 0x44, (byte) 0x88,})); + + Assertions.assertArrayEquals( + new byte[]{(byte) 0xF5, (byte) 0x79, (byte) 0xF5, (byte) 0x78, (byte) 0x02, (byte) 0x46, (byte) 0x02, (byte) 0x46, (byte) 0xAD, (byte) 0xB8, (byte) 0x24, (byte) 0x68, (byte) 0xAD, (byte) 0xB8, + (byte) 0x24, (byte) 0xEF,}, + SivMode.dbl(new byte[]{(byte) 0xFA, (byte) 0xBC, (byte) 0xFA, (byte) 0xBC, (byte) 0x01, (byte) 0x23, (byte) 0x01, (byte) 0x23, (byte) 0x56, (byte) 0xDC, (byte) 0x12, (byte) 0x34, (byte) 0x56, + (byte) 0xDC, (byte) 0x12, (byte) 0x34,})); + } + + @Test + public void testXor() { + Assertions.assertArrayEquals(new byte[]{}, SivMode.xor(new byte[0], new byte[0])); + Assertions.assertArrayEquals(new byte[3], SivMode.xor(new byte[3], new byte[3])); + Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xor(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); + Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xor(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); + Assertions.assertArrayEquals(new byte[]{(byte) 0xAB, (byte) 0x87, (byte) 0x34}, SivMode.xor(new byte[]{(byte) 0xB9, (byte) 0xB3, (byte) 0x62}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56, (byte) 0x78})); + } + + @Test + public void testXorend() { + Assertions.assertArrayEquals(new byte[]{}, SivMode.xorend(new byte[0], new byte[0])); + Assertions.assertArrayEquals(new byte[3], SivMode.xorend(new byte[3], new byte[3])); + Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xorend(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); + Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xorend(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); + Assertions.assertArrayEquals(new byte[]{(byte) 0xB8, (byte) 0xA9, (byte) 0xAB, (byte) 0x87, (byte) 0x34}, + SivMode.xorend(new byte[]{(byte) 0xB8, (byte) 0xA9, (byte) 0xB9, (byte) 0xB3, (byte) 0x62}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56})); + Assertions.assertArrayEquals(new byte[]{(byte) 0x23, (byte) 0x80, (byte) 0x32, (byte) 0xEF, (byte) 0xDE, (byte) 0xCD, (byte) 0xAB, (byte) 0x87, (byte) 0x34}, + SivMode.xorend(new byte[]{(byte) 0x23, (byte) 0x80, (byte) 0x32, (byte) 0xEF, (byte) 0xDE, (byte) 0xCD, (byte) 0xB9, (byte) 0xB3, (byte) 0x62,}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56})); + } - SivMode.shiftLeft(new byte[] {(byte) 0x89, (byte) 0x65, (byte) 0x43, (byte) 0x21}, output); - Assertions.assertArrayEquals(new byte[] {(byte) 0x12, (byte) 0xCA, (byte) 0x86, (byte) 0x42}, output); - } - - @Test - public void testDouble() { - Assertions.assertArrayEquals( - new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00,}, - SivMode.dbl(new byte[] {(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00,})); - - Assertions.assertArrayEquals( - new byte[] {(byte) 0x22, (byte) 0x44, (byte) 0x66, (byte) 0x88, (byte) 0xAA, (byte) 0xCC, (byte) 0xEF, (byte) 0x10, (byte) 0x22, (byte) 0x44, (byte) 0x66, (byte) 0x88, (byte) 0x22, (byte) 0x44, - (byte) 0x22, (byte) 0x44,}, - SivMode.dbl(new byte[] {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x11, - (byte) 0x22, (byte) 0x11, (byte) 0x22,})); - - Assertions.assertArrayEquals( - new byte[] {(byte) 0x10, (byte) 0x88, (byte) 0x44, (byte) 0x23, (byte) 0x32, (byte) 0xEE, (byte) 0xAA, (byte) 0x66, (byte) 0x22, (byte) 0x66, (byte) 0xAA, (byte) 0xEE, (byte) 0x22, (byte) 0x44, - (byte) 0x89, (byte) 0x97,}, - SivMode.dbl(new byte[] {(byte) 0x88, (byte) 0x44, (byte) 0x22, (byte) 0x11, (byte) 0x99, (byte) 0x77, (byte) 0x55, (byte) 0x33, (byte) 0x11, (byte) 0x33, (byte) 0x55, (byte) 0x77, (byte) 0x11, - (byte) 0x22, (byte) 0x44, (byte) 0x88,})); - - Assertions.assertArrayEquals( - new byte[] {(byte) 0xF5, (byte) 0x79, (byte) 0xF5, (byte) 0x78, (byte) 0x02, (byte) 0x46, (byte) 0x02, (byte) 0x46, (byte) 0xAD, (byte) 0xB8, (byte) 0x24, (byte) 0x68, (byte) 0xAD, (byte) 0xB8, - (byte) 0x24, (byte) 0xEF,}, - SivMode.dbl(new byte[] {(byte) 0xFA, (byte) 0xBC, (byte) 0xFA, (byte) 0xBC, (byte) 0x01, (byte) 0x23, (byte) 0x01, (byte) 0x23, (byte) 0x56, (byte) 0xDC, (byte) 0x12, (byte) 0x34, (byte) 0x56, - (byte) 0xDC, (byte) 0x12, (byte) 0x34,})); - } - - @Test - public void testXor() { - Assertions.assertArrayEquals(new byte[] {}, SivMode.xor(new byte[0], new byte[0])); - Assertions.assertArrayEquals(new byte[3], SivMode.xor(new byte[3], new byte[3])); - Assertions.assertArrayEquals(new byte[] {(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xor(new byte[] {(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[] {(byte) 0xFE, (byte) 0x57, (byte) 0x82})); - Assertions.assertArrayEquals(new byte[] {(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xor(new byte[] {(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[] {(byte) 0xFE, (byte) 0x57, (byte) 0x82})); - Assertions.assertArrayEquals(new byte[] {(byte) 0xAB, (byte) 0x87, (byte) 0x34}, SivMode.xor(new byte[] {(byte) 0xB9, (byte) 0xB3, (byte) 0x62}, new byte[] {(byte) 0x12, (byte) 0x34, (byte) 0x56, (byte) 0x78})); - } - - @Test - public void testXorend() { - Assertions.assertArrayEquals(new byte[] {}, SivMode.xorend(new byte[0], new byte[0])); - Assertions.assertArrayEquals(new byte[3], SivMode.xorend(new byte[3], new byte[3])); - Assertions.assertArrayEquals(new byte[] {(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xorend(new byte[] {(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[] {(byte) 0xFE, (byte) 0x57, (byte) 0x82})); - Assertions.assertArrayEquals(new byte[] {(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xorend(new byte[] {(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[] {(byte) 0xFE, (byte) 0x57, (byte) 0x82})); - Assertions.assertArrayEquals(new byte[] {(byte) 0xB8, (byte) 0xA9, (byte) 0xAB, (byte) 0x87, (byte) 0x34}, - SivMode.xorend(new byte[] {(byte) 0xB8, (byte) 0xA9, (byte) 0xB9, (byte) 0xB3, (byte) 0x62}, new byte[] {(byte) 0x12, (byte) 0x34, (byte) 0x56})); - Assertions.assertArrayEquals(new byte[] {(byte) 0x23, (byte) 0x80, (byte) 0x32, (byte) 0xEF, (byte) 0xDE, (byte) 0xCD, (byte) 0xAB, (byte) 0x87, (byte) 0x34}, - SivMode.xorend(new byte[] {(byte) 0x23, (byte) 0x80, (byte) 0x32, (byte) 0xEF, (byte) 0xDE, (byte) 0xCD, (byte) 0xB9, (byte) 0xB3, (byte) 0x62,}, new byte[] {(byte) 0x12, (byte) 0x34, (byte) 0x56})); } @TestFactory @@ -536,29 +374,32 @@ public Stream testGeneratedTestCases() { throw new UncheckedIOException(e); } }); - SivMode sivMode = new SivMode(); return lines.map(EncryptionTestCase::fromLine).map(testCase -> { int testIdx = testCase.getTestCaseNumber(); + SivMode sivMode = new SivMode(testCase.getKey()); return DynamicContainer.dynamicContainer("test case " + testIdx, Arrays.asList( DynamicTest.dynamicTest("decrypt", () -> { - byte[] actualPlaintext = sivMode.decrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getCiphertext(), testCase.getAssociatedData()); + byte[] actualPlaintext = sivMode.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); Assertions.assertArrayEquals(testCase.getPlaintext(), actualPlaintext); }), DynamicTest.dynamicTest("encrypt", () -> { - byte[] actualCiphertext = sivMode.encrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getPlaintext(), testCase.getAssociatedData()); + byte[] actualCiphertext = sivMode.encrypt(testCase.getPlaintext(), testCase.getAssociatedData()); Assertions.assertArrayEquals(testCase.getCiphertext(), actualCiphertext); }), - DynamicTest.dynamicTest("decrypt fails due to tampered MAC", () -> { - byte[] macKey = testCase.getMacKey(); + DynamicTest.dynamicTest("decrypt fails due to tampered mac key", () -> { + byte[] key = testCase.getKey(); - // Pick some arbitrary key byte to tamper with - int tamperedByteIndex = testIdx % macKey.length; + // Pick some arbitrary byte from first half of key (i.e. the MAC key) to tamper with + int halfKeyLen = key.length / 2; + int tamperedByteIndex = testIdx % halfKeyLen; // Flip a single bit - macKey[tamperedByteIndex] ^= 0x10; + key[tamperedByteIndex] ^= 0x10; + + SivMode sivWithTamperedKey = new SivMode(key); Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCtrKey(), macKey, testCase.getCiphertext(), testCase.getAssociatedData()); + sivWithTamperedKey.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); }); }), DynamicTest.dynamicTest("decrypt fails due to tampered ciphertext", () -> { @@ -571,7 +412,7 @@ public Stream testGeneratedTestCases() { ciphertext[tamperedByteIndex] ^= 0x10; Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCtrKey(), testCase.getMacKey(), ciphertext, testCase.getAssociatedData()); + sivMode.decrypt(ciphertext, testCase.getAssociatedData()); }); }), DynamicTest.dynamicTest("decrypt fails due to tampered associated data", () -> { @@ -591,7 +432,7 @@ public Stream testGeneratedTestCases() { ad[adIdx][tamperedByteIndex] ^= 0x04; Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getCiphertext(), ad); + sivMode.decrypt(testCase.getCiphertext(), ad); }); // Restore ad to original value @@ -610,7 +451,7 @@ public Stream testGeneratedTestCases() { System.arraycopy(ad, 0, prependedAd, 1, ad.length); Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getCiphertext(), prependedAd); + sivMode.decrypt(testCase.getCiphertext(), prependedAd); }); }), DynamicTest.dynamicTest("decrypt fails due to appended associated data", () -> { @@ -625,16 +466,11 @@ public Stream testGeneratedTestCases() { System.arraycopy(ad, 0, appendedAd, 0, ad.length); Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getCiphertext(), appendedAd); + sivMode.decrypt(testCase.getCiphertext(), appendedAd); }); }) )); }); } - private Provider getSunJceProvider() { - Provider provider = Security.getProvider("SunJCE"); - Assertions.assertNotNull(provider); - return provider; - } } From 3841573c8dcdc74571da848921c72cffa645f3ff Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 25 Oct 2025 23:42:18 +0200 Subject: [PATCH 04/25] more cleanup --- pom.xml | 7 -- src/main/java/org/cryptomator/siv/CMac.java | 23 +++--- .../java/org/cryptomator/siv/SivMode.java | 77 ++---------------- src/main/java/org/cryptomator/siv/Utils.java | 67 ++++++++++++++++ src/main/java9/module-info.java | 2 - .../java/org/cryptomator/siv/SivModeTest.java | 63 --------------- .../java/org/cryptomator/siv/UtilsTest.java | 78 +++++++++++++++++++ 7 files changed, 162 insertions(+), 155 deletions(-) create mode 100644 src/main/java/org/cryptomator/siv/Utils.java create mode 100644 src/test/java/org/cryptomator/siv/UtilsTest.java diff --git a/pom.xml b/pom.xml index 3d10e06..136f578 100644 --- a/pom.xml +++ b/pom.xml @@ -53,13 +53,6 @@ - - org.jetbrains - annotations - 26.0.2 - provided - - org.junit.jupiter diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java index 64ecb24..c76a103 100644 --- a/src/main/java/org/cryptomator/siv/CMac.java +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -14,6 +14,9 @@ import java.security.spec.AlgorithmParameterSpec; import java.util.Arrays; +import static org.cryptomator.siv.Utils.dbl; +import static org.cryptomator.siv.Utils.xor; + /** * AES-CMAC (Cipher-based Message Authentication Code). * Specs: RFC 4493. @@ -56,8 +59,8 @@ protected void engineInit(Key key, AlgorithmParameterSpec params) throws Invalid try { // L = AES_encrypt(K, const_Zero) encryptBlock(cipher, L, L); - this.k1 = SivMode.dbl(L); - this.k2 = SivMode.dbl(k1); + this.k1 = dbl(L); + this.k2 = dbl(k1); } finally { Arrays.fill(L, (byte) 0); } @@ -92,7 +95,7 @@ protected void engineUpdate(byte[] input, int offset, int len) { // https://www.rfc-editor.org/rfc/rfc4493.html#section-2.4 Step 6 private void processBlock() { - SivMode.xor(x, buffer, y); // Y := X XOR M_i; + xor(x, buffer, y); // Y := X XOR M_i; encryptBlock(cipher, y, x); // X := AES-128(K,Y); bufferPos = 0; } @@ -107,7 +110,7 @@ protected byte[] engineDoFinal() { byte[] m_last; if (flag) { // M_last := M_n XOR K1; - m_last = SivMode.xor(buffer, k1); + m_last = xor(buffer, k1); } else { // M_last := padding(M_n) XOR K2; // @@ -118,11 +121,11 @@ protected byte[] engineDoFinal() { if (bufferPos + 1 < BLOCK_SIZE) { Arrays.fill(buffer, bufferPos + 1, BLOCK_SIZE, (byte) 0x00); // followed by '0' bits } - m_last = SivMode.xor(buffer, k2); + m_last = xor(buffer, k2); } // Step 7: - SivMode.xor(m_last, x, y); // Y := M_last XOR X; + xor(m_last, x, y); // Y := M_last XOR X; try { byte[] t = new byte[BLOCK_SIZE]; encryptBlock(cipher, y, t); // T := AES-128(K,Y); @@ -179,12 +182,4 @@ public static byte[] tag(byte[] key, byte[] message) { cmac.engineUpdate(message, 0, message.length); return cmac.engineDoFinal(); } - - /** - * Verify CMAC tag - */ - public static boolean verify(byte[] key, byte[] message, byte[] tag) { - byte[] computedTag = tag(key, message); - return MessageDigest.isEqual(computedTag, tag); - } } diff --git a/src/main/java/org/cryptomator/siv/SivMode.java b/src/main/java/org/cryptomator/siv/SivMode.java index 0d2b4a3..e43b88c 100644 --- a/src/main/java/org/cryptomator/siv/SivMode.java +++ b/src/main/java/org/cryptomator/siv/SivMode.java @@ -1,7 +1,5 @@ package org.cryptomator.siv; -import org.jetbrains.annotations.VisibleForTesting; - import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -14,13 +12,17 @@ import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import static org.cryptomator.siv.Utils.dbl; +import static org.cryptomator.siv.Utils.pad; +import static org.cryptomator.siv.Utils.xor; +import static org.cryptomator.siv.Utils.xorend; + /** * Implements the RFC 5297 SIV mode. */ public final class SivMode { private static final byte[] BYTES_ZERO = new byte[16]; - private static final byte DOUBLING_CONST = (byte) 0x87; private final SecretKey macKey; private final SecretKey ctrKey; @@ -65,7 +67,7 @@ public SivMode(byte[] key) { * @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. - * @throws IllegalArgumentException if either of the two keys is of invalid length. + * @throws IllegalArgumentException if either param exceeds the limits for safe use. */ public byte[] encrypt(byte[] plaintext, byte[]... associatedData) { // Check if plaintext length will cause overflows @@ -89,7 +91,6 @@ public byte[] encrypt(byte[] plaintext, byte[]... associatedData) { * @param ciphertext Your ciphertext, which shall be encrypted. * @param associatedData Optional associated data, which needs to be authenticated during decryption. * @return Plaintext byte array. - * @throws IllegalArgumentException If the either of the two keys is of invalid length. * @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. */ @@ -117,7 +118,7 @@ public byte[] decrypt(byte[] ciphertext, byte[]... associatedData) throws Unauth } } - @VisibleForTesting + // visible for testing byte[] computeCtr(byte[] input, final byte[] iv) { // clear out the 31st and 63rd (rightmost) bit: final byte[] adjustedIv = Arrays.copyOf(iv, 16); @@ -136,7 +137,7 @@ byte[] computeCtr(byte[] input, final byte[] iv) { } } - @VisibleForTesting + // visible for testing byte[] s2v(byte[] plaintext, byte[]... associatedData) throws IllegalArgumentException { // Maximum permitted AD length is the block size in bits - 2 if (associatedData.length > 126) { @@ -170,66 +171,4 @@ private static byte[] mac(CMac mac, byte[] in) { return mac.engineDoFinal(); } - // First bit 1, following bits 0. - private static byte[] pad(byte[] in) { - final byte[] result = Arrays.copyOf(in, 16); - result[in.length] = (byte) 0x80; - return result; - } - - // Code taken from {@link org.bouncycastle.crypto.macs.CMac} - @VisibleForTesting - static int shiftLeft(byte[] block, byte[] output) { - int i = block.length; - int bit = 0; - while (--i >= 0) { - int b = block[i] & 0xff; - output[i] = (byte) ((b << 1) | bit); - bit = (b >>> 7) & 1; - } - return bit; - } - - // Code taken from {@link org.bouncycastle.crypto.macs.CMac} - @VisibleForTesting - static byte[] dbl(byte[] in) { - byte[] ret = new byte[in.length]; - int carry = shiftLeft(in, ret); - int xor = 0xff & DOUBLING_CONST; - - /* - * NOTE: This construction is an attempt at a constant-time implementation. - */ - int mask = (-carry) & 0xff; - ret[in.length - 1] ^= xor & mask; - - return ret; - } - - @VisibleForTesting - static byte[] xor(byte[] in1, byte[] in2) { - assert in1.length <= in2.length : "Length of first input must be <= length of second input."; - final byte[] result = new byte[in1.length]; - xor(in1, in2, result); - return result; - } - - static void xor(byte[] in1, byte[] in2, byte[] result) { - assert result.length <= in1.length && result.length <= in2.length : "All inputs must have the same length."; - for (int i = 0; i < result.length; i++) { - result[i] = (byte) (in1[i] ^ in2[i]); - } - } - - @VisibleForTesting - static byte[] xorend(byte[] in1, byte[] in2) { - assert in1.length >= in2.length : "Length of first input must be >= length of second input."; - final byte[] result = Arrays.copyOf(in1, in1.length); - final int diff = in1.length - in2.length; - for (int i = 0; i < in2.length; i++) { - result[i + diff] = (byte) (result[i + diff] ^ in2[i]); - } - return result; - } - } diff --git a/src/main/java/org/cryptomator/siv/Utils.java b/src/main/java/org/cryptomator/siv/Utils.java new file mode 100644 index 0000000..f832d6a --- /dev/null +++ b/src/main/java/org/cryptomator/siv/Utils.java @@ -0,0 +1,67 @@ +package org.cryptomator.siv; + +import java.util.Arrays; + +public class Utils { + + private static final byte DOUBLING_CONST = (byte) 0x87; + + // First bit 1, following bits 0. + static byte[] pad(byte[] in) { + final byte[] result = Arrays.copyOf(in, 16); + result[in.length] = (byte) 0x80; + return result; + } + + // Code taken from {@link org.bouncycastle.crypto.macs.CMac} + static int shiftLeft(byte[] block, byte[] output) { + int i = block.length; + int bit = 0; + while (--i >= 0) { + int b = block[i] & 0xff; + output[i] = (byte) ((b << 1) | bit); + bit = (b >>> 7) & 1; + } + return bit; + } + + // Code taken from {@link org.bouncycastle.crypto.macs.CMac} + static byte[] dbl(byte[] in) { + byte[] ret = new byte[in.length]; + int carry = shiftLeft(in, ret); + int xor = 0xff & DOUBLING_CONST; + + /* + * NOTE: This construction is an attempt at a constant-time implementation. + */ + int mask = (-carry) & 0xff; + ret[in.length - 1] ^= xor & mask; + + return ret; + } + + static byte[] xor(byte[] in1, byte[] in2) { + assert in1.length <= in2.length : "Length of first input must be <= length of second input."; + final byte[] result = new byte[in1.length]; + xor(in1, in2, result); + return result; + } + + static void xor(byte[] in1, byte[] in2, byte[] result) { + assert result.length <= in1.length && result.length <= in2.length : "All inputs must have the same length."; + for (int i = 0; i < result.length; i++) { + result[i] = (byte) (in1[i] ^ in2[i]); + } + } + + static byte[] xorend(byte[] in1, byte[] in2) { + assert in1.length >= in2.length : "Length of first input must be >= length of second input."; + final byte[] result = Arrays.copyOf(in1, in1.length); + final int diff = in1.length - in2.length; + for (int i = 0; i < in2.length; i++) { + result[i + diff] = (byte) (result[i + diff] ^ in2[i]); + } + return result; + } + +} diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java index 98a03f6..cfc6925 100644 --- a/src/main/java9/module-info.java +++ b/src/main/java9/module-info.java @@ -1,5 +1,3 @@ module org.cryptomator.siv { - requires static org.jetbrains.annotations; - exports org.cryptomator.siv; } \ No newline at end of file diff --git a/src/test/java/org/cryptomator/siv/SivModeTest.java b/src/test/java/org/cryptomator/siv/SivModeTest.java index 2177b32..f64a362 100644 --- a/src/test/java/org/cryptomator/siv/SivModeTest.java +++ b/src/test/java/org/cryptomator/siv/SivModeTest.java @@ -295,70 +295,7 @@ public void testSivDecrypt() throws UnauthenticCiphertextException, IllegalBlock @Nested public class HelperMethods { - @Test - public void testShiftLeft() { - final byte[] output = new byte[4]; - - SivMode.shiftLeft(new byte[]{(byte) 0x77, (byte) 0x3A, (byte) 0x87, (byte) 0x22}, output); - Assertions.assertArrayEquals(new byte[]{(byte) 0xEE, (byte) 0x75, (byte) 0x0E, (byte) 0x44}, output); - - SivMode.shiftLeft(new byte[]{(byte) 0x56, (byte) 0x12, (byte) 0x34, (byte) 0x99}, output); - Assertions.assertArrayEquals(new byte[]{(byte) 0xAC, (byte) 0x24, (byte) 0x69, (byte) 0x32}, output); - - SivMode.shiftLeft(new byte[]{(byte) 0xCF, (byte) 0xAB, (byte) 0xBA, (byte) 0x78}, output); - Assertions.assertArrayEquals(new byte[]{(byte) 0x9F, (byte) 0x57, (byte) 0x74, (byte) 0xF0}, output); - - SivMode.shiftLeft(new byte[]{(byte) 0x89, (byte) 0x65, (byte) 0x43, (byte) 0x21}, output); - Assertions.assertArrayEquals(new byte[]{(byte) 0x12, (byte) 0xCA, (byte) 0x86, (byte) 0x42}, output); - } - - @Test - public void testDouble() { - Assertions.assertArrayEquals( - new byte[]{(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00,}, - SivMode.dbl(new byte[]{(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - (byte) 0x00, (byte) 0x00, (byte) 0x00,})); - - Assertions.assertArrayEquals( - new byte[]{(byte) 0x22, (byte) 0x44, (byte) 0x66, (byte) 0x88, (byte) 0xAA, (byte) 0xCC, (byte) 0xEF, (byte) 0x10, (byte) 0x22, (byte) 0x44, (byte) 0x66, (byte) 0x88, (byte) 0x22, (byte) 0x44, - (byte) 0x22, (byte) 0x44,}, - SivMode.dbl(new byte[]{(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x11, - (byte) 0x22, (byte) 0x11, (byte) 0x22,})); - - Assertions.assertArrayEquals( - new byte[]{(byte) 0x10, (byte) 0x88, (byte) 0x44, (byte) 0x23, (byte) 0x32, (byte) 0xEE, (byte) 0xAA, (byte) 0x66, (byte) 0x22, (byte) 0x66, (byte) 0xAA, (byte) 0xEE, (byte) 0x22, (byte) 0x44, - (byte) 0x89, (byte) 0x97,}, - SivMode.dbl(new byte[]{(byte) 0x88, (byte) 0x44, (byte) 0x22, (byte) 0x11, (byte) 0x99, (byte) 0x77, (byte) 0x55, (byte) 0x33, (byte) 0x11, (byte) 0x33, (byte) 0x55, (byte) 0x77, (byte) 0x11, - (byte) 0x22, (byte) 0x44, (byte) 0x88,})); - - Assertions.assertArrayEquals( - new byte[]{(byte) 0xF5, (byte) 0x79, (byte) 0xF5, (byte) 0x78, (byte) 0x02, (byte) 0x46, (byte) 0x02, (byte) 0x46, (byte) 0xAD, (byte) 0xB8, (byte) 0x24, (byte) 0x68, (byte) 0xAD, (byte) 0xB8, - (byte) 0x24, (byte) 0xEF,}, - SivMode.dbl(new byte[]{(byte) 0xFA, (byte) 0xBC, (byte) 0xFA, (byte) 0xBC, (byte) 0x01, (byte) 0x23, (byte) 0x01, (byte) 0x23, (byte) 0x56, (byte) 0xDC, (byte) 0x12, (byte) 0x34, (byte) 0x56, - (byte) 0xDC, (byte) 0x12, (byte) 0x34,})); - } - @Test - public void testXor() { - Assertions.assertArrayEquals(new byte[]{}, SivMode.xor(new byte[0], new byte[0])); - Assertions.assertArrayEquals(new byte[3], SivMode.xor(new byte[3], new byte[3])); - Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xor(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); - Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xor(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); - Assertions.assertArrayEquals(new byte[]{(byte) 0xAB, (byte) 0x87, (byte) 0x34}, SivMode.xor(new byte[]{(byte) 0xB9, (byte) 0xB3, (byte) 0x62}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56, (byte) 0x78})); - } - - @Test - public void testXorend() { - Assertions.assertArrayEquals(new byte[]{}, SivMode.xorend(new byte[0], new byte[0])); - Assertions.assertArrayEquals(new byte[3], SivMode.xorend(new byte[3], new byte[3])); - Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xorend(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); - Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, SivMode.xorend(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); - Assertions.assertArrayEquals(new byte[]{(byte) 0xB8, (byte) 0xA9, (byte) 0xAB, (byte) 0x87, (byte) 0x34}, - SivMode.xorend(new byte[]{(byte) 0xB8, (byte) 0xA9, (byte) 0xB9, (byte) 0xB3, (byte) 0x62}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56})); - Assertions.assertArrayEquals(new byte[]{(byte) 0x23, (byte) 0x80, (byte) 0x32, (byte) 0xEF, (byte) 0xDE, (byte) 0xCD, (byte) 0xAB, (byte) 0x87, (byte) 0x34}, - SivMode.xorend(new byte[]{(byte) 0x23, (byte) 0x80, (byte) 0x32, (byte) 0xEF, (byte) 0xDE, (byte) 0xCD, (byte) 0xB9, (byte) 0xB3, (byte) 0x62,}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56})); - } } diff --git a/src/test/java/org/cryptomator/siv/UtilsTest.java b/src/test/java/org/cryptomator/siv/UtilsTest.java new file mode 100644 index 0000000..1691890 --- /dev/null +++ b/src/test/java/org/cryptomator/siv/UtilsTest.java @@ -0,0 +1,78 @@ +package org.cryptomator.siv; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.cryptomator.siv.Utils.dbl; +import static org.cryptomator.siv.Utils.shiftLeft; +import static org.cryptomator.siv.Utils.xor; +import static org.cryptomator.siv.Utils.xorend; + +class UtilsTest { + + @Test + public void testShiftLeft() { + final byte[] output = new byte[4]; + + shiftLeft(new byte[]{(byte) 0x77, (byte) 0x3A, (byte) 0x87, (byte) 0x22}, output); + Assertions.assertArrayEquals(new byte[]{(byte) 0xEE, (byte) 0x75, (byte) 0x0E, (byte) 0x44}, output); + + shiftLeft(new byte[]{(byte) 0x56, (byte) 0x12, (byte) 0x34, (byte) 0x99}, output); + Assertions.assertArrayEquals(new byte[]{(byte) 0xAC, (byte) 0x24, (byte) 0x69, (byte) 0x32}, output); + + shiftLeft(new byte[]{(byte) 0xCF, (byte) 0xAB, (byte) 0xBA, (byte) 0x78}, output); + Assertions.assertArrayEquals(new byte[]{(byte) 0x9F, (byte) 0x57, (byte) 0x74, (byte) 0xF0}, output); + + shiftLeft(new byte[]{(byte) 0x89, (byte) 0x65, (byte) 0x43, (byte) 0x21}, output); + Assertions.assertArrayEquals(new byte[]{(byte) 0x12, (byte) 0xCA, (byte) 0x86, (byte) 0x42}, output); + } + + @Test + public void testDouble() { + Assertions.assertArrayEquals( + new byte[]{(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00,}, + dbl(new byte[]{(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00,})); + + Assertions.assertArrayEquals( + new byte[]{(byte) 0x22, (byte) 0x44, (byte) 0x66, (byte) 0x88, (byte) 0xAA, (byte) 0xCC, (byte) 0xEF, (byte) 0x10, (byte) 0x22, (byte) 0x44, (byte) 0x66, (byte) 0x88, (byte) 0x22, (byte) 0x44, + (byte) 0x22, (byte) 0x44,}, + dbl(new byte[]{(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x11, + (byte) 0x22, (byte) 0x11, (byte) 0x22,})); + + Assertions.assertArrayEquals( + new byte[]{(byte) 0x10, (byte) 0x88, (byte) 0x44, (byte) 0x23, (byte) 0x32, (byte) 0xEE, (byte) 0xAA, (byte) 0x66, (byte) 0x22, (byte) 0x66, (byte) 0xAA, (byte) 0xEE, (byte) 0x22, (byte) 0x44, + (byte) 0x89, (byte) 0x97,}, + dbl(new byte[]{(byte) 0x88, (byte) 0x44, (byte) 0x22, (byte) 0x11, (byte) 0x99, (byte) 0x77, (byte) 0x55, (byte) 0x33, (byte) 0x11, (byte) 0x33, (byte) 0x55, (byte) 0x77, (byte) 0x11, + (byte) 0x22, (byte) 0x44, (byte) 0x88,})); + + Assertions.assertArrayEquals( + new byte[]{(byte) 0xF5, (byte) 0x79, (byte) 0xF5, (byte) 0x78, (byte) 0x02, (byte) 0x46, (byte) 0x02, (byte) 0x46, (byte) 0xAD, (byte) 0xB8, (byte) 0x24, (byte) 0x68, (byte) 0xAD, (byte) 0xB8, + (byte) 0x24, (byte) 0xEF,}, + dbl(new byte[]{(byte) 0xFA, (byte) 0xBC, (byte) 0xFA, (byte) 0xBC, (byte) 0x01, (byte) 0x23, (byte) 0x01, (byte) 0x23, (byte) 0x56, (byte) 0xDC, (byte) 0x12, (byte) 0x34, (byte) 0x56, + (byte) 0xDC, (byte) 0x12, (byte) 0x34,})); + } + + @Test + public void testXor() { + Assertions.assertArrayEquals(new byte[]{}, xor(new byte[0], new byte[0])); + Assertions.assertArrayEquals(new byte[3], xor(new byte[3], new byte[3])); + Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, xor(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); + Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, xor(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); + Assertions.assertArrayEquals(new byte[]{(byte) 0xAB, (byte) 0x87, (byte) 0x34}, xor(new byte[]{(byte) 0xB9, (byte) 0xB3, (byte) 0x62}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56, (byte) 0x78})); + } + + @Test + public void testXorend() { + Assertions.assertArrayEquals(new byte[]{}, xorend(new byte[0], new byte[0])); + Assertions.assertArrayEquals(new byte[3], xorend(new byte[3], new byte[3])); + Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, xorend(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); + Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, xorend(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); + Assertions.assertArrayEquals(new byte[]{(byte) 0xB8, (byte) 0xA9, (byte) 0xAB, (byte) 0x87, (byte) 0x34}, + xorend(new byte[]{(byte) 0xB8, (byte) 0xA9, (byte) 0xB9, (byte) 0xB3, (byte) 0x62}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56})); + Assertions.assertArrayEquals(new byte[]{(byte) 0x23, (byte) 0x80, (byte) 0x32, (byte) 0xEF, (byte) 0xDE, (byte) 0xCD, (byte) 0xAB, (byte) 0x87, (byte) 0x34}, + xorend(new byte[]{(byte) 0x23, (byte) 0x80, (byte) 0x32, (byte) 0xEF, (byte) 0xDE, (byte) 0xCD, (byte) 0xB9, (byte) 0xB3, (byte) 0x62,}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56})); + } + +} \ No newline at end of file From 7c54f74dd8662b687449a0f528ecbf37019d5452 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 26 Oct 2025 00:12:10 +0200 Subject: [PATCH 05/25] create `java.security.Provider` --- src/main/java/org/cryptomator/siv/CMac.java | 3 +-- .../java/org/cryptomator/siv/SivMode.java | 15 ++++++++------ .../java/org/cryptomator/siv/SivProvider.java | 15 ++++++++++++++ src/main/java9/module-info.java | 4 ++++ .../META-INF/services/java.security.Provider | 1 + .../java/org/cryptomator/siv/SivModeTest.java | 7 ------- .../org/cryptomator/siv/SivProviderTest.java | 20 +++++++++++++++++++ 7 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 src/main/java/org/cryptomator/siv/SivProvider.java create mode 100644 src/main/resources/META-INF/services/java.security.Provider create mode 100644 src/test/java/org/cryptomator/siv/SivProviderTest.java diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java index c76a103..48c25ec 100644 --- a/src/main/java/org/cryptomator/siv/CMac.java +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -9,7 +9,6 @@ import javax.crypto.spec.SecretKeySpec; import java.security.InvalidKeyException; import java.security.Key; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.spec.AlgorithmParameterSpec; import java.util.Arrays; @@ -21,7 +20,7 @@ * AES-CMAC (Cipher-based Message Authentication Code). * Specs: RFC 4493. */ -class CMac extends MacSpi { +public class CMac extends MacSpi { private static final int BLOCK_SIZE = 16; // 128 bits for AES private static final String AES_ALGORITHM = "AES"; diff --git a/src/main/java/org/cryptomator/siv/SivMode.java b/src/main/java/org/cryptomator/siv/SivMode.java index e43b88c..2eea9d6 100644 --- a/src/main/java/org/cryptomator/siv/SivMode.java +++ b/src/main/java/org/cryptomator/siv/SivMode.java @@ -3,6 +3,7 @@ import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; +import javax.crypto.Mac; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; @@ -27,7 +28,7 @@ public final class SivMode { private final SecretKey macKey; private final SecretKey ctrKey; - private final CMac cmac; + private final Mac cmac; private final Cipher ctrCipher; /** @@ -47,11 +48,14 @@ public SivMode(byte[] key) { System.arraycopy(key, macKey.length, ctrKey, 0, ctrKey.length); // K2 = rightmost(K, len(K)/2); this.macKey = new SecretKeySpec(macKey, "AES"); this.ctrKey = new SecretKeySpec(ctrKey, "AES"); - this.cmac = new CMac(); + try { - cmac.engineInit(this.macKey, null); + this.cmac = Mac.getInstance("CMAC", SivProvider.INSTANCE); + cmac.init(this.macKey); } catch (InvalidKeyException e) { throw new IllegalArgumentException(e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Failed to find CMAC in SivProvider.", e); } try { @@ -166,9 +170,8 @@ byte[] s2v(byte[] plaintext, byte[]... associatedData) throws IllegalArgumentExc return mac(cmac, t); } - private static byte[] mac(CMac mac, byte[] in) { - mac.engineUpdate(in, 0, in.length); - return mac.engineDoFinal(); + private static byte[] mac(Mac mac, byte[] in) { + return mac.doFinal(in); } } diff --git a/src/main/java/org/cryptomator/siv/SivProvider.java b/src/main/java/org/cryptomator/siv/SivProvider.java new file mode 100644 index 0000000..81c3cac --- /dev/null +++ b/src/main/java/org/cryptomator/siv/SivProvider.java @@ -0,0 +1,15 @@ +package org.cryptomator.siv; + +import java.security.Provider; +import java.util.HashMap; + +public class SivProvider extends Provider { + + public static final SivProvider INSTANCE = new SivProvider(); + + public SivProvider() { + super("SIV", 2.0, ""); + putService(new Service(this, "Mac", "CMAC", CMac.class.getName(), null, new HashMap<>())); + } + +} diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java index cfc6925..4961b89 100644 --- a/src/main/java9/module-info.java +++ b/src/main/java9/module-info.java @@ -1,3 +1,7 @@ +import java.security.Provider; + module org.cryptomator.siv { exports org.cryptomator.siv; + + provides Provider with org.cryptomator.siv.SivProvider; } \ No newline at end of file diff --git a/src/main/resources/META-INF/services/java.security.Provider b/src/main/resources/META-INF/services/java.security.Provider new file mode 100644 index 0000000..beef22c --- /dev/null +++ b/src/main/resources/META-INF/services/java.security.Provider @@ -0,0 +1 @@ +org.cryptomator.siv.SivProvider \ No newline at end of file diff --git a/src/test/java/org/cryptomator/siv/SivModeTest.java b/src/test/java/org/cryptomator/siv/SivModeTest.java index f64a362..0a5cda3 100644 --- a/src/test/java/org/cryptomator/siv/SivModeTest.java +++ b/src/test/java/org/cryptomator/siv/SivModeTest.java @@ -290,13 +290,6 @@ public void testSivDecrypt() throws UnauthenticCiphertextException, IllegalBlock Assertions.assertArrayEquals(plaintext, result); } - } - - @Nested - public class HelperMethods { - - - } @TestFactory diff --git a/src/test/java/org/cryptomator/siv/SivProviderTest.java b/src/test/java/org/cryptomator/siv/SivProviderTest.java new file mode 100644 index 0000000..3784342 --- /dev/null +++ b/src/test/java/org/cryptomator/siv/SivProviderTest.java @@ -0,0 +1,20 @@ +package org.cryptomator.siv; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import javax.crypto.Mac; +import java.security.Security; + +class SivProviderTest { + + @Test + public void getMac() { + Security.addProvider(SivProvider.INSTANCE); + + Assertions.assertDoesNotThrow(() -> Mac.getInstance("CMAC", SivProvider.INSTANCE)); + Assertions.assertDoesNotThrow(() -> Mac.getInstance("CMAC", "SIV")); + Assertions.assertDoesNotThrow(() -> Mac.getInstance("CMAC")); + } + +} \ No newline at end of file From a57175070aa309bc19f9fc862aa6ce60d1905988 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 18 Nov 2025 16:40:30 +0100 Subject: [PATCH 06/25] implement CipherSpi --- README.md | 10 +- src/main/java/org/cryptomator/siv/CMac.java | 2 +- .../java/org/cryptomator/siv/SivCipher.java | 150 ++++++++++++++++++ .../siv/{SivMode.java => SivEngine.java} | 62 +++++--- .../java/org/cryptomator/siv/SivProvider.java | 1 + .../siv/UnauthenticCiphertextException.java | 20 --- .../org/cryptomator/siv/package-info.java | 6 +- .../{SivModeTest.java => SivEngineTest.java} | 85 +++++----- .../org/cryptomator/siv/SivModeBenchmark.java | 13 +- .../org/cryptomator/siv/SivProviderTest.java | 32 ++++ 10 files changed, 287 insertions(+), 94 deletions(-) create mode 100644 src/main/java/org/cryptomator/siv/SivCipher.java rename src/main/java/org/cryptomator/siv/{SivMode.java => SivEngine.java} (75%) delete mode 100644 src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java rename src/test/java/org/cryptomator/siv/{SivModeTest.java => SivEngineTest.java} (83%) diff --git a/README.md b/README.md index 23d89a1..b41500d 100644 --- a/README.md +++ b/README.md @@ -28,16 +28,16 @@ ## Usage ```java -private static final SivMode AES_SIV = new SivMode(); +SivMode AES_SIV = new SivMode(key); public void encrypt() { - byte[] encrypted = AES_SIV.encrypt(ctrKey, macKey, "hello world".getBytes()); - byte[] decrypted = AES_SIV.decrypt(ctrKey, macKey, encrypted); + byte[] encrypted = AES_SIV.encrypt("hello world".getBytes()); + byte[] decrypted = AES_SIV.decrypt(encrypted); } public void encryptWithAssociatedData() { - byte[] encrypted = AES_SIV.encrypt(ctrKey, macKey, "hello world".getBytes(), "associated".getBytes(), "data".getBytes()); - byte[] decrypted = AES_SIV.decrypt(ctrKey, macKey, encrypted, "associated".getBytes(), "data".getBytes()); + byte[] encrypted = AES_SIV.encrypt("hello world".getBytes(), "associated".getBytes(), "data".getBytes()); + byte[] decrypted = AES_SIV.decrypt(encrypted, "associated".getBytes(), "data".getBytes()); } ``` diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java index 48c25ec..02ba255 100644 --- a/src/main/java/org/cryptomator/siv/CMac.java +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -22,7 +22,7 @@ */ public class CMac extends MacSpi { - private static final int BLOCK_SIZE = 16; // 128 bits for AES + static final int BLOCK_SIZE = 16; // 128 bits for AES private static final String AES_ALGORITHM = "AES"; private static final String AES_ECB_NO_PADDING = "AES/ECB/NoPadding"; diff --git a/src/main/java/org/cryptomator/siv/SivCipher.java b/src/main/java/org/cryptomator/siv/SivCipher.java new file mode 100644 index 0000000..de611dc --- /dev/null +++ b/src/main/java/org/cryptomator/siv/SivCipher.java @@ -0,0 +1,150 @@ +package org.cryptomator.siv; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherSpi; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.ShortBufferException; +import java.nio.ByteBuffer; +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class SivCipher extends CipherSpi { + + private static final byte[] EMPTY = new byte[0]; + + private int opmode; + private byte[] key; + private List aad; + private byte[] inputBuffer; + + @Override + protected void engineSetMode(String mode) throws NoSuchAlgorithmException { + if (!mode.equalsIgnoreCase("SIV")) { + throw new NoSuchAlgorithmException("Mode must be SIV"); + } + } + + @Override + protected void engineSetPadding(String padding) throws NoSuchPaddingException { + if (!padding.equalsIgnoreCase("NoPadding")) { + throw new NoSuchPaddingException("Padding must be NoPadding"); + } + } + + @Override + protected int engineGetBlockSize() { + return 16; + } + + @Override + protected int engineGetOutputSize(int inputLen) { + if (opmode == Cipher.ENCRYPT_MODE || opmode == Cipher.WRAP_MODE) { + return 16 + inputLen; + } else if (opmode == Cipher.DECRYPT_MODE || opmode == Cipher.UNWRAP_MODE) { + return inputLen - 16; + } else { + throw new IllegalStateException("Invalid opmode " + this.opmode); + } + } + + @Override + protected byte[] engineGetIV() { + return null; + } + + @Override + protected AlgorithmParameters engineGetParameters() { + return null; + } + + @Override + protected void engineInit(int opmode, Key key, SecureRandom random) throws InvalidKeyException { + byte[] keybytes = key.getEncoded(); + if (keybytes.length != 64 && keybytes.length != 48 && keybytes.length != 32) { + throw new InvalidKeyException("Key length must be 256, 384, or 512 bits."); + } + this.opmode = opmode; + this.key = keybytes; + this.aad = new ArrayList<>(); + this.inputBuffer = EMPTY; + } + + @Override + protected void engineInit(int opmode, Key key, AlgorithmParameterSpec params, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { + engineInit(opmode, key, random); + } + + @Override + protected void engineInit(int opmode, Key key, AlgorithmParameters params, SecureRandom random) throws InvalidKeyException, InvalidAlgorithmParameterException { + engineInit(opmode, key, random); + } + + @Override + protected void engineUpdateAAD(byte[] src, int offset, int len) { + byte[] bytes = Arrays.copyOfRange(src, offset, offset + len); + this.aad.add(bytes); + } + + @Override + protected void engineUpdateAAD(ByteBuffer src) { + byte[] bytes = new byte[src.remaining()]; + src.get(bytes); + this.aad.add(bytes); + } + + @Override + protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) { + int oldLen = inputBuffer.length; + inputBuffer = Arrays.copyOf(inputBuffer, oldLen + inputLen); + System.arraycopy(input, inputOffset, inputBuffer, oldLen, inputLen); + return EMPTY; + } + + @Override + protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) { + engineUpdate(input, inputOffset, inputLen); + return 0; + } + + @Override + protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) throws IllegalBlockSizeException, BadPaddingException { + byte[] output = new byte[engineGetOutputSize(inputBuffer.length + inputLen)]; + try { + engineDoFinal(input, inputOffset, inputLen, output, 0); + } catch (ShortBufferException e) { + throw new IllegalStateException(e); + } + return output; + } + + @Override + protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + int availableSpace = output.length - outputOffset; + if (availableSpace < engineGetOutputSize(inputBuffer.length + inputLen)) { + throw new ShortBufferException(); + } + engineUpdate(input, inputOffset, inputLen); + + SivEngine siv = new SivEngine(this.key); + byte[][] aad = this.aad.toArray(new byte[this.aad.size()][]); + if (this.opmode == Cipher.ENCRYPT_MODE || this.opmode == Cipher.WRAP_MODE) { + return siv.encrypt(inputBuffer, output, outputOffset, aad); + } else if (this.opmode == Cipher.DECRYPT_MODE || this.opmode == Cipher.UNWRAP_MODE) { + byte[] plaintext = siv.decrypt(inputBuffer, aad); + System.arraycopy(plaintext, 0, output, outputOffset, plaintext.length); + return plaintext.length; + } else { + throw new IllegalStateException("Invalid opmode " + this.opmode); + } + } +} diff --git a/src/main/java/org/cryptomator/siv/SivMode.java b/src/main/java/org/cryptomator/siv/SivEngine.java similarity index 75% rename from src/main/java/org/cryptomator/siv/SivMode.java rename to src/main/java/org/cryptomator/siv/SivEngine.java index 2eea9d6..b4f58a6 100644 --- a/src/main/java/org/cryptomator/siv/SivMode.java +++ b/src/main/java/org/cryptomator/siv/SivEngine.java @@ -1,11 +1,13 @@ package org.cryptomator.siv; +import javax.crypto.AEADBadTagException; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.Mac; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.security.InvalidAlgorithmParameterException; @@ -21,8 +23,9 @@ /** * Implements the RFC 5297 SIV mode. */ -public final class SivMode { +public final class SivEngine { + private static final int IV_LENGTH = CMac.BLOCK_SIZE; private static final byte[] BYTES_ZERO = new byte[16]; private final SecretKey macKey; @@ -36,7 +39,7 @@ public final class SivMode { * * @param key A 256, 384, or 512 bit key. The first half is used for nonce generation, the second half for encryption */ - public SivMode(byte[] key) { + public SivEngine(byte[] key) { if (key.length != 64 && key.length != 48 && key.length != 32) { throw new IllegalArgumentException("Key length must be 256, 384, or 512 bits."); } @@ -74,19 +77,29 @@ public SivMode(byte[] key) { * @throws IllegalArgumentException if either param exceeds the limits for safe use. */ public byte[] encrypt(byte[] plaintext, byte[]... associatedData) { + final byte[] ciphertext = new byte[16 + plaintext.length]; + try { + int encrypted = encrypt(plaintext, ciphertext, 0, associatedData); + assert encrypted == ciphertext.length; + } catch (ShortBufferException e) { + throw new IllegalStateException(e); + } + return ciphertext; + } + + public int encrypt(byte[] input, byte[] output, int outputOffset, byte[]... associatedData) throws ShortBufferException { // Check if plaintext length will cause overflows - if (plaintext.length > (Integer.MAX_VALUE - 16)) { + if (input.length > (Integer.MAX_VALUE - IV_LENGTH)) { throw new IllegalArgumentException("Plaintext is too long"); } - final byte[] iv = s2v(plaintext, associatedData); - final byte[] ciphertext = computeCtr(plaintext, iv); - - // concat IV + ciphertext: - final byte[] result = new byte[iv.length + ciphertext.length]; - System.arraycopy(iv, 0, result, 0, iv.length); - System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length); - return result; + if (output.length - outputOffset < IV_LENGTH + input.length) { + throw new ShortBufferException(); + } + byte[] iv = s2v(input, associatedData); + assert iv.length == IV_LENGTH; + System.arraycopy(iv, 0, output, 0, IV_LENGTH); + return IV_LENGTH + computeCtr(input, iv, output, IV_LENGTH); } /** @@ -95,16 +108,16 @@ public byte[] encrypt(byte[] plaintext, byte[]... associatedData) { * @param ciphertext Your ciphertext, which shall be encrypted. * @param associatedData Optional associated data, which needs to be authenticated during decryption. * @return Plaintext byte array. - * @throws UnauthenticCiphertextException If the authentication failed, e.g. because ciphertext and/or associatedData are corrupted. + * @throws AEADBadTagException 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(byte[] ciphertext, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException { - if (ciphertext.length < 16) { + public byte[] decrypt(byte[] ciphertext, byte[]... associatedData) throws AEADBadTagException, IllegalBlockSizeException { + if (ciphertext.length < IV_LENGTH) { throw new IllegalBlockSizeException("Input length must be greater than or equal 16."); } - final byte[] iv = Arrays.copyOf(ciphertext, 16); - final byte[] actualCiphertext = Arrays.copyOfRange(ciphertext, 16, ciphertext.length); + final byte[] iv = Arrays.copyOf(ciphertext, IV_LENGTH); + final byte[] actualCiphertext = Arrays.copyOfRange(ciphertext, IV_LENGTH, ciphertext.length); final byte[] plaintext = computeCtr(actualCiphertext, iv); final byte[] control = s2v(plaintext, associatedData); @@ -118,12 +131,24 @@ public byte[] decrypt(byte[] ciphertext, byte[]... associatedData) throws Unauth if (diff == 0) { return plaintext; } else { - throw new UnauthenticCiphertextException("authentication in SIV decryption failed"); + throw new AEADBadTagException("authentication in SIV decryption failed"); } } // visible for testing byte[] computeCtr(byte[] input, final byte[] iv) { + byte[] output = new byte[input.length]; + try { + int processed = computeCtr(input, iv, output, 0); + assert processed == output.length; + } catch (ShortBufferException e) { + throw new IllegalStateException(e); + } + return output; + } + + // visible for testing + int computeCtr(byte[] input, final byte[] iv, byte[] output, int outputOffset) throws ShortBufferException { // clear out the 31st and 63rd (rightmost) bit: final byte[] adjustedIv = Arrays.copyOf(iv, 16); adjustedIv[8] = (byte) (adjustedIv[8] & 0x7F); @@ -131,7 +156,7 @@ byte[] computeCtr(byte[] input, final byte[] iv) { try { ctrCipher.init(Cipher.ENCRYPT_MODE, ctrKey, new IvParameterSpec(adjustedIv)); - return ctrCipher.doFinal(input); + return ctrCipher.doFinal(input, 0, input.length, output, outputOffset); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { throw new IllegalArgumentException("Key or IV invalid."); } catch (BadPaddingException e) { @@ -173,5 +198,4 @@ byte[] s2v(byte[] plaintext, byte[]... associatedData) throws IllegalArgumentExc private static byte[] mac(Mac mac, byte[] in) { return mac.doFinal(in); } - } diff --git a/src/main/java/org/cryptomator/siv/SivProvider.java b/src/main/java/org/cryptomator/siv/SivProvider.java index 81c3cac..999898f 100644 --- a/src/main/java/org/cryptomator/siv/SivProvider.java +++ b/src/main/java/org/cryptomator/siv/SivProvider.java @@ -10,6 +10,7 @@ public class SivProvider extends Provider { public SivProvider() { super("SIV", 2.0, ""); putService(new Service(this, "Mac", "CMAC", CMac.class.getName(), null, new HashMap<>())); + putService(new Service(this, "Cipher", "AES/SIV/NoPadding", SivCipher.class.getName(), null, new HashMap<>())); } } diff --git a/src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java b/src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java deleted file mode 100644 index 83dafb9..0000000 --- a/src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.cryptomator.siv; - -import javax.crypto.BadPaddingException; - -/** - * Drop-in replacement for {@link javax.crypto.AEADBadTagException}, which is not available on some older Android systems. - */ -public class UnauthenticCiphertextException extends BadPaddingException { - - /** - * Constructs a UnauthenticCiphertextException with the specified - * detail message. - * - * @param message the detail message. - */ - public UnauthenticCiphertextException(String message) { - super(message); - } - -} diff --git a/src/main/java/org/cryptomator/siv/package-info.java b/src/main/java/org/cryptomator/siv/package-info.java index c598bd1..bb9e864 100644 --- a/src/main/java/org/cryptomator/siv/package-info.java +++ b/src/main/java/org/cryptomator/siv/package-info.java @@ -1,8 +1,8 @@ /** * Java implementation of RFC 5297 SIV Authenticated Encryption. *

- * Use an instance of the {@link org.cryptomator.siv.SivMode} class to - * {@link org.cryptomator.siv.SivMode#encrypt(byte[], byte[]...) encrypt} or - * {@link org.cryptomator.siv.SivMode#decrypt(byte[], byte[]...) decrypt} data. + * Use an instance of the {@link org.cryptomator.siv.SivEngine} class to + * {@link org.cryptomator.siv.SivEngine#encrypt(byte[], byte[]...) encrypt} or + * {@link org.cryptomator.siv.SivEngine#decrypt(byte[], byte[]...) decrypt} data. */ package org.cryptomator.siv; \ No newline at end of file diff --git a/src/test/java/org/cryptomator/siv/SivModeTest.java b/src/test/java/org/cryptomator/siv/SivEngineTest.java similarity index 83% rename from src/test/java/org/cryptomator/siv/SivModeTest.java rename to src/test/java/org/cryptomator/siv/SivEngineTest.java index 0a5cda3..4d1ff74 100644 --- a/src/test/java/org/cryptomator/siv/SivModeTest.java +++ b/src/test/java/org/cryptomator/siv/SivEngineTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import javax.crypto.AEADBadTagException; import javax.crypto.IllegalBlockSizeException; import java.io.BufferedReader; import java.io.IOException; @@ -23,7 +24,7 @@ /** * Official RFC 5297 test vector taken from https://tools.ietf.org/html/rfc5297#appendix-A.1 and https://tools.ietf.org/html/rfc5297#appendix-A.2 */ -public class SivModeTest { +public class SivEngineTest { @Nested public class ParameterValidation { @@ -33,16 +34,16 @@ public class ParameterValidation { public void testCreateWithInvalidKeyLength(int keylen) { byte[] key = new byte[keylen]; - Assertions.assertThrows(IllegalArgumentException.class, () -> new SivMode(key)); + Assertions.assertThrows(IllegalArgumentException.class, () -> new SivEngine(key)); } @Test public void testDecryptWithInvalidBlockSize() { final byte[] key = new byte[32]; - SivMode sivMode = new SivMode(key); + SivEngine siv = new SivEngine(key); Assertions.assertThrows(IllegalBlockSizeException.class, () -> { - sivMode.decrypt(new byte[10]); + siv.decrypt(new byte[10]); }); } @@ -51,9 +52,9 @@ public void testEncryptAssociatedDataLimit() { final byte[] key = new byte[32]; final byte[] plaintext = new byte[30]; - SivMode sivMode = new SivMode(key); + SivEngine siv = new SivEngine(key); Assertions.assertThrows(IllegalArgumentException.class, () -> { - sivMode.encrypt(plaintext, new byte[127][0]); + siv.encrypt(plaintext, new byte[127][0]); }); } @@ -62,21 +63,21 @@ public void testDecryptAssociatedDataLimit() { final byte[] key = new byte[32]; final byte[] plaintext = new byte[80]; - SivMode sivMode = new SivMode(key); + SivEngine siv = new SivEngine(key); Assertions.assertThrows(IllegalArgumentException.class, () -> { - sivMode.decrypt(plaintext, new byte[127][0]); + siv.decrypt(plaintext, new byte[127][0]); }); } } @ParameterizedTest @ValueSource(ints = {32, 48, 64}) - public void testEncryptionAndDecryption(int keylen) throws UnauthenticCiphertextException, IllegalBlockSizeException { + public void testEncryptionAndDecryption(int keylen) throws AEADBadTagException, IllegalBlockSizeException { final byte[] key = new byte[keylen]; - final SivMode sivMode = new SivMode(key); + final SivEngine siv = new SivEngine(key); final byte[] cleartext = "hello world".getBytes(); - final byte[] ciphertext = sivMode.encrypt(cleartext); - final byte[] decrypted = sivMode.decrypt(ciphertext); + final byte[] ciphertext = siv.encrypt(cleartext); + final byte[] decrypted = siv.decrypt(ciphertext); Assertions.assertArrayEquals(cleartext, decrypted); } @@ -128,7 +129,7 @@ public void testComputeCtr() { (byte) 0x43, (byte) 0x45, (byte) 0xc4, (byte) 0xa6, // (byte) 0x23, (byte) 0xb2, (byte) 0xf0, (byte) 0x8f}; - final byte[] result = new SivMode(key).computeCtr(new byte[16], ctr); + final byte[] result = new SivEngine(key).computeCtr(new byte[16], ctr); Assertions.assertArrayEquals(expected, result); } @@ -139,19 +140,19 @@ public void testS2v() { (byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, // (byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93}; - final byte[] result = new SivMode(key).s2v(plaintext, ad); + final byte[] result = new SivEngine(key).s2v(plaintext, ad); Assertions.assertArrayEquals(expected, result); } @Test public void testSivEncrypt() { - final byte[] result = new SivMode(key).encrypt(plaintext, ad); + final byte[] result = new SivEngine(key).encrypt(plaintext, ad); Assertions.assertArrayEquals(ciphertext, result); } @Test - public void testSivDecrypt() throws UnauthenticCiphertextException, IllegalBlockSizeException { - final byte[] result = new SivMode(key).decrypt(ciphertext, ad); + public void testSivDecrypt() throws AEADBadTagException, IllegalBlockSizeException { + final byte[] result = new SivEngine(key).decrypt(ciphertext, ad); Assertions.assertArrayEquals(plaintext, result); } @@ -160,9 +161,9 @@ public void testSivDecryptWithInvalidKey() { final byte[] invalidKey = Arrays.copyOf(key, key.length); invalidKey[invalidKey.length - 1] = 0x00; - SivMode sivMode = new SivMode(invalidKey); - Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(ciphertext, ad); + SivEngine siv = new SivEngine(invalidKey); + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(ciphertext, ad); }); } @@ -171,9 +172,9 @@ public void testSivDecryptWithInvalidCiphertext() { final byte[] invalidCiphertext = Arrays.copyOf(ciphertext, ciphertext.length); invalidCiphertext[invalidCiphertext.length - 1] = 0x00; - SivMode sivMode = new SivMode(key); - Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(invalidCiphertext); + SivEngine siv = new SivEngine(key); + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(invalidCiphertext); }); } @@ -181,9 +182,9 @@ public void testSivDecryptWithInvalidCiphertext() { public void testSivDecryptWithTruncatedCiphertext() { final byte[] invalidCiphertext = Arrays.copyOf(ciphertext, 15); - SivMode sivMode = new SivMode(key); + SivEngine siv = new SivEngine(key); Assertions.assertThrows(IllegalBlockSizeException.class, () -> { - sivMode.decrypt(invalidCiphertext); + siv.decrypt(invalidCiphertext); }); } @@ -274,19 +275,19 @@ public void testComputeCtr() { (byte) 0x1b, (byte) 0x12, (byte) 0x34, (byte) 0x8e, // (byte) 0xbc, (byte) 0x19, (byte) 0x5e, (byte) 0xc7}; - final byte[] result = new SivMode(key).computeCtr(new byte[48], ctr); + final byte[] result = new SivEngine(key).computeCtr(new byte[48], ctr); Assertions.assertArrayEquals(expected, result); } @Test public void testSivEncrypt() { - final byte[] result = new SivMode(key).encrypt(plaintext, ad1, ad2, nonce); + final byte[] result = new SivEngine(key).encrypt(plaintext, ad1, ad2, nonce); Assertions.assertArrayEquals(ciphertext, result); } @Test - public void testSivDecrypt() throws UnauthenticCiphertextException, IllegalBlockSizeException { - final byte[] result = new SivMode(key).decrypt(ciphertext, ad1, ad2, nonce); + public void testSivDecrypt() throws AEADBadTagException, IllegalBlockSizeException { + final byte[] result = new SivEngine(key).decrypt(ciphertext, ad1, ad2, nonce); Assertions.assertArrayEquals(plaintext, result); } @@ -306,14 +307,14 @@ public Stream testGeneratedTestCases() { }); return lines.map(EncryptionTestCase::fromLine).map(testCase -> { int testIdx = testCase.getTestCaseNumber(); - SivMode sivMode = new SivMode(testCase.getKey()); + SivEngine siv = new SivEngine(testCase.getKey()); return DynamicContainer.dynamicContainer("test case " + testIdx, Arrays.asList( DynamicTest.dynamicTest("decrypt", () -> { - byte[] actualPlaintext = sivMode.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); + byte[] actualPlaintext = siv.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); Assertions.assertArrayEquals(testCase.getPlaintext(), actualPlaintext); }), DynamicTest.dynamicTest("encrypt", () -> { - byte[] actualCiphertext = sivMode.encrypt(testCase.getPlaintext(), testCase.getAssociatedData()); + byte[] actualCiphertext = siv.encrypt(testCase.getPlaintext(), testCase.getAssociatedData()); Assertions.assertArrayEquals(testCase.getCiphertext(), actualCiphertext); }), DynamicTest.dynamicTest("decrypt fails due to tampered mac key", () -> { @@ -326,9 +327,9 @@ public Stream testGeneratedTestCases() { // Flip a single bit key[tamperedByteIndex] ^= 0x10; - SivMode sivWithTamperedKey = new SivMode(key); + SivEngine sivWithTamperedKey = new SivEngine(key); - Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { + Assertions.assertThrows(AEADBadTagException.class, () -> { sivWithTamperedKey.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); }); }), @@ -341,8 +342,8 @@ public Stream testGeneratedTestCases() { // Flip a single bit ciphertext[tamperedByteIndex] ^= 0x10; - Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(ciphertext, testCase.getAssociatedData()); + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(ciphertext, testCase.getAssociatedData()); }); }), DynamicTest.dynamicTest("decrypt fails due to tampered associated data", () -> { @@ -361,8 +362,8 @@ public Stream testGeneratedTestCases() { // Flip a single bit ad[adIdx][tamperedByteIndex] ^= 0x04; - Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCiphertext(), ad); + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(testCase.getCiphertext(), ad); }); // Restore ad to original value @@ -380,8 +381,8 @@ public Stream testGeneratedTestCases() { prependedAd[0] = new byte[testIdx % 16]; System.arraycopy(ad, 0, prependedAd, 1, ad.length); - Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCiphertext(), prependedAd); + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(testCase.getCiphertext(), prependedAd); }); }), DynamicTest.dynamicTest("decrypt fails due to appended associated data", () -> { @@ -395,8 +396,8 @@ public Stream testGeneratedTestCases() { appendedAd[ad.length] = new byte[testIdx % 16]; System.arraycopy(ad, 0, appendedAd, 0, ad.length); - Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCiphertext(), appendedAd); + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(testCase.getCiphertext(), appendedAd); }); }) )); diff --git a/src/test/java/org/cryptomator/siv/SivModeBenchmark.java b/src/test/java/org/cryptomator/siv/SivModeBenchmark.java index 7cf27f4..1bfc271 100644 --- a/src/test/java/org/cryptomator/siv/SivModeBenchmark.java +++ b/src/test/java/org/cryptomator/siv/SivModeBenchmark.java @@ -7,12 +7,14 @@ import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; +import javax.crypto.AEADBadTagException; import javax.crypto.IllegalBlockSizeException; import java.util.Arrays; import java.util.concurrent.TimeUnit; @@ -29,22 +31,25 @@ public class SivModeBenchmark { private int run; private final byte[] key = new byte[32]; - private final byte[] cleartextData = new byte[1000]; + @Param({"1024", "1048576", "10485760"}) + private int cleartextDataSize; + private byte[] cleartextData; private final byte[] associatedData = new byte[100]; - private SivMode siv; + private SivEngine siv; @Setup(Level.Trial) public void shuffleData() { run++; Arrays.fill(key, (byte) (run & 0xFF)); - siv = new SivMode(key); + siv = new SivEngine(key); + cleartextData = new byte[cleartextDataSize]; Arrays.fill(cleartextData, (byte) (run & 0xFF)); Arrays.fill(associatedData, (byte) (run & 0xFF)); } @Benchmark - public void benchmarkJce(Blackhole bh) throws UnauthenticCiphertextException, IllegalBlockSizeException { + public void benchmarkJce(Blackhole bh) throws AEADBadTagException, IllegalBlockSizeException { byte[] encrypted = siv.encrypt(cleartextData, associatedData); byte[] decrypted = siv.decrypt(encrypted, associatedData); Assertions.assertArrayEquals(cleartextData, decrypted); diff --git a/src/test/java/org/cryptomator/siv/SivProviderTest.java b/src/test/java/org/cryptomator/siv/SivProviderTest.java index 3784342..f417974 100644 --- a/src/test/java/org/cryptomator/siv/SivProviderTest.java +++ b/src/test/java/org/cryptomator/siv/SivProviderTest.java @@ -3,7 +3,16 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.Security; class SivProviderTest { @@ -17,4 +26,27 @@ public void getMac() { Assertions.assertDoesNotThrow(() -> Mac.getInstance("CMAC")); } + @Test + public void getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + Security.addProvider(SivProvider.INSTANCE); + + Assertions.assertDoesNotThrow(() -> Cipher.getInstance("AES/SIV/NoPadding", SivProvider.INSTANCE)); + Assertions.assertDoesNotThrow(() -> Cipher.getInstance("AES/SIV/NoPadding", "SIV")); + Assertions.assertDoesNotThrow(() -> Cipher.getInstance("AES/SIV/NoPadding")); + + Cipher cipher = Cipher.getInstance("AES/SIV/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(new byte[64], "AES")); + cipher.updateAAD(new byte[1]); + cipher.updateAAD(new byte[2]); + cipher.update("hello".getBytes(StandardCharsets.UTF_8)); + byte[] ciphertext = cipher.doFinal("world".getBytes(StandardCharsets.UTF_8)); + + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(new byte[64], "AES")); + cipher.updateAAD(new byte[1]); + cipher.updateAAD(new byte[2]); + byte[] plaintext = cipher.doFinal(ciphertext); + + Assertions.assertArrayEquals("helloworld".getBytes(StandardCharsets.UTF_8), plaintext); + } + } \ No newline at end of file From 2f97464fa52574fefc46164246b134f866ddd506 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 18 Nov 2025 16:41:17 +0100 Subject: [PATCH 07/25] in-situ `dbl`, `xor`, `xorend` --- src/main/java/org/cryptomator/siv/CMac.java | 18 ++++---- .../java/org/cryptomator/siv/SivEngine.java | 2 +- src/main/java/org/cryptomator/siv/Utils.java | 44 ++++++++----------- 3 files changed, 27 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java index 02ba255..a4672e4 100644 --- a/src/main/java/org/cryptomator/siv/CMac.java +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -34,8 +34,7 @@ public class CMac extends MacSpi { // MAC state: private final byte[] buffer = new byte[BLOCK_SIZE]; private int bufferPos = 0; - private final byte[] x = new byte[BLOCK_SIZE]; // X := const_Zero; - private final byte[] y = new byte[BLOCK_SIZE]; + private final byte[] xy = new byte[BLOCK_SIZE]; // X := const_Zero; private int msgLen = 0; @Override @@ -58,8 +57,8 @@ protected void engineInit(Key key, AlgorithmParameterSpec params) throws Invalid try { // L = AES_encrypt(K, const_Zero) encryptBlock(cipher, L, L); - this.k1 = dbl(L); - this.k2 = dbl(k1); + this.k1 = dbl(L).clone(); + this.k2 = dbl(L).clone(); } finally { Arrays.fill(L, (byte) 0); } @@ -94,8 +93,8 @@ protected void engineUpdate(byte[] input, int offset, int len) { // https://www.rfc-editor.org/rfc/rfc4493.html#section-2.4 Step 6 private void processBlock() { - xor(x, buffer, y); // Y := X XOR M_i; - encryptBlock(cipher, y, x); // X := AES-128(K,Y); + xor(xy, buffer); // Y := X XOR M_i; + encryptBlock(cipher, xy, xy); // X := AES-128(K,Y); bufferPos = 0; } @@ -124,10 +123,10 @@ protected byte[] engineDoFinal() { } // Step 7: - xor(m_last, x, y); // Y := M_last XOR X; + xor(xy, m_last); // Y := M_last XOR X; try { byte[] t = new byte[BLOCK_SIZE]; - encryptBlock(cipher, y, t); // T := AES-128(K,Y); + encryptBlock(cipher, xy, t); // T := AES-128(K,Y); return t; } finally { engineReset(); @@ -139,8 +138,7 @@ protected void engineReset() { bufferPos = 0; msgLen = 0; Arrays.fill(buffer, (byte) 0); - Arrays.fill(x, (byte) 0); - Arrays.fill(y, (byte) 0); + Arrays.fill(xy, (byte) 0); } private static void encryptBlock(Cipher cipher, byte[] block, byte[] output) { diff --git a/src/main/java/org/cryptomator/siv/SivEngine.java b/src/main/java/org/cryptomator/siv/SivEngine.java index b4f58a6..fe519db 100644 --- a/src/main/java/org/cryptomator/siv/SivEngine.java +++ b/src/main/java/org/cryptomator/siv/SivEngine.java @@ -187,7 +187,7 @@ byte[] s2v(byte[] plaintext, byte[]... associatedData) throws IllegalArgumentExc final byte[] t; if (plaintext.length >= 16) { - t = xorend(plaintext, d); + t = xorend(Arrays.copyOf(plaintext, plaintext.length), d); } else { t = xor(dbl(d), pad(plaintext)); } diff --git a/src/main/java/org/cryptomator/siv/Utils.java b/src/main/java/org/cryptomator/siv/Utils.java index f832d6a..78714d9 100644 --- a/src/main/java/org/cryptomator/siv/Utils.java +++ b/src/main/java/org/cryptomator/siv/Utils.java @@ -13,55 +13,47 @@ static byte[] pad(byte[] in) { return result; } - // Code taken from {@link org.bouncycastle.crypto.macs.CMac} static int shiftLeft(byte[] block, byte[] output) { - int i = block.length; - int bit = 0; - while (--i >= 0) { - int b = block[i] & 0xff; - output[i] = (byte) ((b << 1) | bit); - bit = (b >>> 7) & 1; + int carry = 0; + + // Left shift by 1 bit + for (int i = block.length - 1; i >= 0; i--) { + byte b = (byte) (block[i] & 0xff); + output[i] = (byte) ((b << 1) | carry); + carry = (b & 0x80) >>> 7; } - return bit; + + return carry; } - // Code taken from {@link org.bouncycastle.crypto.macs.CMac} - static byte[] dbl(byte[] in) { - byte[] ret = new byte[in.length]; - int carry = shiftLeft(in, ret); + static byte[] dbl(byte[] data) { + int carry = shiftLeft(data, data); int xor = 0xff & DOUBLING_CONST; /* * NOTE: This construction is an attempt at a constant-time implementation. */ int mask = (-carry) & 0xff; - ret[in.length - 1] ^= xor & mask; + data[data.length - 1] ^= xor & mask; - return ret; + return data; } static byte[] xor(byte[] in1, byte[] in2) { assert in1.length <= in2.length : "Length of first input must be <= length of second input."; - final byte[] result = new byte[in1.length]; - xor(in1, in2, result); - return result; - } - - static void xor(byte[] in1, byte[] in2, byte[] result) { - assert result.length <= in1.length && result.length <= in2.length : "All inputs must have the same length."; - for (int i = 0; i < result.length; i++) { - result[i] = (byte) (in1[i] ^ in2[i]); + for (int i = 0; i < in1.length; i++) { + in1[i] = (byte) (in1[i] ^ in2[i]); } + return in1; } static byte[] xorend(byte[] in1, byte[] in2) { assert in1.length >= in2.length : "Length of first input must be >= length of second input."; - final byte[] result = Arrays.copyOf(in1, in1.length); final int diff = in1.length - in2.length; for (int i = 0; i < in2.length; i++) { - result[i + diff] = (byte) (result[i + diff] ^ in2[i]); + in1[i + diff] = (byte) (in1[i + diff] ^ in2[i]); } - return result; + return in1; } } From ca27a0ffc197da09d948a7f0c0274c0d61d77949 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 18 Nov 2025 22:30:30 +0100 Subject: [PATCH 08/25] use different in/out buffers for `cipher.doFinal` to avoid byte[] allocations in `com.sun.crypto.provider.CipherCore.doFinal(byte[], int, int, byte[], int)` --- src/main/java/org/cryptomator/siv/CMac.java | 20 ++++++++++--------- .../java/org/cryptomator/siv/SivEngine.java | 3 ++- src/main/java/org/cryptomator/siv/Utils.java | 8 ++++++-- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java index a4672e4..b6616c7 100644 --- a/src/main/java/org/cryptomator/siv/CMac.java +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -34,7 +34,8 @@ public class CMac extends MacSpi { // MAC state: private final byte[] buffer = new byte[BLOCK_SIZE]; private int bufferPos = 0; - private final byte[] xy = new byte[BLOCK_SIZE]; // X := const_Zero; + private final byte[] x = new byte[BLOCK_SIZE]; // X := const_Zero; + private final byte[] y = new byte[BLOCK_SIZE]; private int msgLen = 0; @Override @@ -93,8 +94,8 @@ protected void engineUpdate(byte[] input, int offset, int len) { // https://www.rfc-editor.org/rfc/rfc4493.html#section-2.4 Step 6 private void processBlock() { - xor(xy, buffer); // Y := X XOR M_i; - encryptBlock(cipher, xy, xy); // X := AES-128(K,Y); + xor(x, buffer, y); // Y := X XOR M_i; + encryptBlock(cipher, y, x); // X := AES-128(K,Y); bufferPos = 0; } @@ -105,10 +106,10 @@ protected byte[] engineDoFinal() { boolean flag = msgLen > 0 && bufferPos % BLOCK_SIZE == 0; // denoting if last block is complete or not // Step 4: - byte[] m_last; + byte[] m_last = new byte[BLOCK_SIZE]; if (flag) { // M_last := M_n XOR K1; - m_last = xor(buffer, k1); + xor(buffer, k1, m_last); } else { // M_last := padding(M_n) XOR K2; // @@ -119,14 +120,14 @@ protected byte[] engineDoFinal() { if (bufferPos + 1 < BLOCK_SIZE) { Arrays.fill(buffer, bufferPos + 1, BLOCK_SIZE, (byte) 0x00); // followed by '0' bits } - m_last = xor(buffer, k2); + xor(buffer, k2, m_last); } // Step 7: - xor(xy, m_last); // Y := M_last XOR X; + xor(x, m_last, y); // Y := M_last XOR X; try { byte[] t = new byte[BLOCK_SIZE]; - encryptBlock(cipher, xy, t); // T := AES-128(K,Y); + encryptBlock(cipher, y, t); // T := AES-128(K,Y); return t; } finally { engineReset(); @@ -138,7 +139,8 @@ protected void engineReset() { bufferPos = 0; msgLen = 0; Arrays.fill(buffer, (byte) 0); - Arrays.fill(xy, (byte) 0); + Arrays.fill(x, (byte) 0); + Arrays.fill(y, (byte) 0); } private static void encryptBlock(Cipher cipher, byte[] block, byte[] output) { diff --git a/src/main/java/org/cryptomator/siv/SivEngine.java b/src/main/java/org/cryptomator/siv/SivEngine.java index fe519db..8513607 100644 --- a/src/main/java/org/cryptomator/siv/SivEngine.java +++ b/src/main/java/org/cryptomator/siv/SivEngine.java @@ -178,11 +178,12 @@ byte[] s2v(byte[] plaintext, byte[]... associatedData) throws IllegalArgumentExc // S1 = associatedData1, S2 = associatedData2, ... Sn = plaintext // Since this method is invoked only by encrypt/decrypt, we always have a plaintext. // Thus n > 0 + assert associatedData.length > 0; byte[] d = mac(cmac, BYTES_ZERO); for (byte[] s : associatedData) { - d = xor(dbl(d), mac(cmac, s)); + xor(dbl(d), mac(cmac, s), d); } final byte[] t; diff --git a/src/main/java/org/cryptomator/siv/Utils.java b/src/main/java/org/cryptomator/siv/Utils.java index 78714d9..8ea1ae6 100644 --- a/src/main/java/org/cryptomator/siv/Utils.java +++ b/src/main/java/org/cryptomator/siv/Utils.java @@ -40,11 +40,15 @@ static byte[] dbl(byte[] data) { } static byte[] xor(byte[] in1, byte[] in2) { + return xor(in1, in2, in1); + } + + static byte[] xor(byte[] in1, byte[] in2, byte[] out) { assert in1.length <= in2.length : "Length of first input must be <= length of second input."; for (int i = 0; i < in1.length; i++) { - in1[i] = (byte) (in1[i] ^ in2[i]); + out[i] = (byte) (in1[i] ^ in2[i]); } - return in1; + return out; } static byte[] xorend(byte[] in1, byte[] in2) { From 945f27d564b617114ae1f82d958a8bf7cfbb0e87 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 18 Nov 2025 22:57:54 +0100 Subject: [PATCH 09/25] fix assertion --- src/main/java/org/cryptomator/siv/CMac.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java index b6616c7..a9a4434 100644 --- a/src/main/java/org/cryptomator/siv/CMac.java +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -77,11 +77,11 @@ protected void engineUpdate(byte input) { @Override protected void engineUpdate(byte[] input, int offset, int len) { - assert bufferPos < BLOCK_SIZE; for (int i = offset; i < offset + len; ) { if (bufferPos == BLOCK_SIZE) { // buffer is full processBlock(); } + assert bufferPos < BLOCK_SIZE; int required = offset + len - i; int available = BLOCK_SIZE - bufferPos; int m = Math.min(required, available); From eb8ce7073676665186f47193b4a01d9742c7cf53 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 18 Nov 2025 22:58:29 +0100 Subject: [PATCH 10/25] avoid `Arrays.copyOfRange` for `xorend` --- .../java/org/cryptomator/siv/SivEngine.java | 22 ++++++++----------- src/main/java/org/cryptomator/siv/Utils.java | 9 -------- .../java/org/cryptomator/siv/UtilsTest.java | 13 ----------- 3 files changed, 9 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/cryptomator/siv/SivEngine.java b/src/main/java/org/cryptomator/siv/SivEngine.java index 8513607..8ee80cc 100644 --- a/src/main/java/org/cryptomator/siv/SivEngine.java +++ b/src/main/java/org/cryptomator/siv/SivEngine.java @@ -18,7 +18,6 @@ import static org.cryptomator.siv.Utils.dbl; import static org.cryptomator.siv.Utils.pad; import static org.cryptomator.siv.Utils.xor; -import static org.cryptomator.siv.Utils.xorend; /** * Implements the RFC 5297 SIV mode. @@ -178,25 +177,22 @@ byte[] s2v(byte[] plaintext, byte[]... associatedData) throws IllegalArgumentExc // S1 = associatedData1, S2 = associatedData2, ... Sn = plaintext // Since this method is invoked only by encrypt/decrypt, we always have a plaintext. // Thus n > 0 - assert associatedData.length > 0; - byte[] d = mac(cmac, BYTES_ZERO); + byte[] d = cmac.doFinal(BYTES_ZERO); for (byte[] s : associatedData) { - xor(dbl(d), mac(cmac, s), d); + xor(dbl(d), cmac.doFinal(s), d); } - final byte[] t; if (plaintext.length >= 16) { - t = xorend(Arrays.copyOf(plaintext, plaintext.length), d); + // T = Sn xorend D + cmac.update(plaintext, 0, plaintext.length - 16); + byte[] end = xor(d, Arrays.copyOfRange(plaintext, plaintext.length - 16, plaintext.length)); + return cmac.doFinal(end); } else { - t = xor(dbl(d), pad(plaintext)); + // T = dbl(D) xor pad(Sn) + byte[] t = xor(dbl(d), pad(plaintext)); + return cmac.doFinal(t); } - - return mac(cmac, t); - } - - private static byte[] mac(Mac mac, byte[] in) { - return mac.doFinal(in); } } diff --git a/src/main/java/org/cryptomator/siv/Utils.java b/src/main/java/org/cryptomator/siv/Utils.java index 8ea1ae6..e7092ce 100644 --- a/src/main/java/org/cryptomator/siv/Utils.java +++ b/src/main/java/org/cryptomator/siv/Utils.java @@ -51,13 +51,4 @@ static byte[] xor(byte[] in1, byte[] in2, byte[] out) { return out; } - static byte[] xorend(byte[] in1, byte[] in2) { - assert in1.length >= in2.length : "Length of first input must be >= length of second input."; - final int diff = in1.length - in2.length; - for (int i = 0; i < in2.length; i++) { - in1[i + diff] = (byte) (in1[i + diff] ^ in2[i]); - } - return in1; - } - } diff --git a/src/test/java/org/cryptomator/siv/UtilsTest.java b/src/test/java/org/cryptomator/siv/UtilsTest.java index 1691890..621c7c7 100644 --- a/src/test/java/org/cryptomator/siv/UtilsTest.java +++ b/src/test/java/org/cryptomator/siv/UtilsTest.java @@ -6,7 +6,6 @@ import static org.cryptomator.siv.Utils.dbl; import static org.cryptomator.siv.Utils.shiftLeft; import static org.cryptomator.siv.Utils.xor; -import static org.cryptomator.siv.Utils.xorend; class UtilsTest { @@ -63,16 +62,4 @@ public void testXor() { Assertions.assertArrayEquals(new byte[]{(byte) 0xAB, (byte) 0x87, (byte) 0x34}, xor(new byte[]{(byte) 0xB9, (byte) 0xB3, (byte) 0x62}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56, (byte) 0x78})); } - @Test - public void testXorend() { - Assertions.assertArrayEquals(new byte[]{}, xorend(new byte[0], new byte[0])); - Assertions.assertArrayEquals(new byte[3], xorend(new byte[3], new byte[3])); - Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, xorend(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); - Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, xorend(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82})); - Assertions.assertArrayEquals(new byte[]{(byte) 0xB8, (byte) 0xA9, (byte) 0xAB, (byte) 0x87, (byte) 0x34}, - xorend(new byte[]{(byte) 0xB8, (byte) 0xA9, (byte) 0xB9, (byte) 0xB3, (byte) 0x62}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56})); - Assertions.assertArrayEquals(new byte[]{(byte) 0x23, (byte) 0x80, (byte) 0x32, (byte) 0xEF, (byte) 0xDE, (byte) 0xCD, (byte) 0xAB, (byte) 0x87, (byte) 0x34}, - xorend(new byte[]{(byte) 0x23, (byte) 0x80, (byte) 0x32, (byte) 0xEF, (byte) 0xDE, (byte) 0xCD, (byte) 0xB9, (byte) 0xB3, (byte) 0x62,}, new byte[]{(byte) 0x12, (byte) 0x34, (byte) 0x56})); - } - } \ No newline at end of file From b619e2367a9932fb9ed892a5c9ae4383bca8f18e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 18 Nov 2025 23:41:44 +0100 Subject: [PATCH 11/25] nil out plaintext if decryption fails --- src/main/java/org/cryptomator/siv/SivEngine.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/cryptomator/siv/SivEngine.java b/src/main/java/org/cryptomator/siv/SivEngine.java index 8ee80cc..36b561c 100644 --- a/src/main/java/org/cryptomator/siv/SivEngine.java +++ b/src/main/java/org/cryptomator/siv/SivEngine.java @@ -130,6 +130,7 @@ public byte[] decrypt(byte[] ciphertext, byte[]... associatedData) throws AEADBa if (diff == 0) { return plaintext; } else { + Arrays.fill(plaintext, (byte) 0x00); throw new AEADBadTagException("authentication in SIV decryption failed"); } } From 36bdb8d6936718034c56ad43c83e1fb9ec003d44 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 18 Nov 2025 23:52:54 +0100 Subject: [PATCH 12/25] avoid copying data from input --- .../java/org/cryptomator/siv/SivCipher.java | 1 + .../java/org/cryptomator/siv/SivEngine.java | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/cryptomator/siv/SivCipher.java b/src/main/java/org/cryptomator/siv/SivCipher.java index de611dc..74cdd46 100644 --- a/src/main/java/org/cryptomator/siv/SivCipher.java +++ b/src/main/java/org/cryptomator/siv/SivCipher.java @@ -140,6 +140,7 @@ protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] if (this.opmode == Cipher.ENCRYPT_MODE || this.opmode == Cipher.WRAP_MODE) { return siv.encrypt(inputBuffer, output, outputOffset, aad); } else if (this.opmode == Cipher.DECRYPT_MODE || this.opmode == Cipher.UNWRAP_MODE) { + // for security reasons we can't write into output directly before checking integrity: byte[] plaintext = siv.decrypt(inputBuffer, aad); System.arraycopy(plaintext, 0, output, outputOffset, plaintext.length); return plaintext.length; diff --git a/src/main/java/org/cryptomator/siv/SivEngine.java b/src/main/java/org/cryptomator/siv/SivEngine.java index 36b561c..773ab3b 100644 --- a/src/main/java/org/cryptomator/siv/SivEngine.java +++ b/src/main/java/org/cryptomator/siv/SivEngine.java @@ -76,7 +76,7 @@ public SivEngine(byte[] key) { * @throws IllegalArgumentException if either param exceeds the limits for safe use. */ public byte[] encrypt(byte[] plaintext, byte[]... associatedData) { - final byte[] ciphertext = new byte[16 + plaintext.length]; + final byte[] ciphertext = new byte[IV_LENGTH + plaintext.length]; try { int encrypted = encrypt(plaintext, ciphertext, 0, associatedData); assert encrypted == ciphertext.length; @@ -86,19 +86,19 @@ public byte[] encrypt(byte[] plaintext, byte[]... associatedData) { return ciphertext; } - public int encrypt(byte[] input, byte[] output, int outputOffset, byte[]... associatedData) throws ShortBufferException { + public int encrypt(byte[] plaintext, byte[] output, int outputOffset, byte[]... associatedData) throws ShortBufferException { // Check if plaintext length will cause overflows - if (input.length > (Integer.MAX_VALUE - IV_LENGTH)) { + if (plaintext.length > (Integer.MAX_VALUE - IV_LENGTH)) { throw new IllegalArgumentException("Plaintext is too long"); } - if (output.length - outputOffset < IV_LENGTH + input.length) { + if (output.length - outputOffset < IV_LENGTH + plaintext.length) { throw new ShortBufferException(); } - byte[] iv = s2v(input, associatedData); + byte[] iv = s2v(plaintext, associatedData); assert iv.length == IV_LENGTH; System.arraycopy(iv, 0, output, 0, IV_LENGTH); - return IV_LENGTH + computeCtr(input, iv, output, IV_LENGTH); + return IV_LENGTH + computeCtr(plaintext, 0, plaintext.length, iv, 0, IV_LENGTH, output, IV_LENGTH); } /** @@ -115,16 +115,20 @@ public byte[] decrypt(byte[] ciphertext, byte[]... associatedData) throws AEADBa throw new IllegalBlockSizeException("Input length must be greater than or equal 16."); } - final byte[] iv = Arrays.copyOf(ciphertext, IV_LENGTH); - final byte[] actualCiphertext = Arrays.copyOfRange(ciphertext, IV_LENGTH, ciphertext.length); - final byte[] plaintext = computeCtr(actualCiphertext, iv); + final byte[] plaintext = new byte[ciphertext.length - IV_LENGTH]; + try { + int decrypted = computeCtr(ciphertext, IV_LENGTH, ciphertext.length - IV_LENGTH, ciphertext, 0, IV_LENGTH, plaintext, 0); + assert decrypted == plaintext.length; + } catch (ShortBufferException e) { + throw new IllegalStateException(e); + } final byte[] control = s2v(plaintext, associatedData); // time-constant comparison (taken from MessageDigest.isEqual in JDK8) - assert iv.length == control.length; + assert control.length == IV_LENGTH; int diff = 0; - for (int i = 0; i < iv.length; i++) { - diff |= iv[i] ^ control[i]; + for (int i = 0; i < IV_LENGTH; i++) { + diff |= ciphertext[i] ^ control[i]; } if (diff == 0) { @@ -139,7 +143,7 @@ public byte[] decrypt(byte[] ciphertext, byte[]... associatedData) throws AEADBa byte[] computeCtr(byte[] input, final byte[] iv) { byte[] output = new byte[input.length]; try { - int processed = computeCtr(input, iv, output, 0); + int processed = computeCtr(input, 0, input.length, iv, 0, iv.length, output, 0); assert processed == output.length; } catch (ShortBufferException e) { throw new IllegalStateException(e); @@ -147,16 +151,16 @@ byte[] computeCtr(byte[] input, final byte[] iv) { return output; } - // visible for testing - int computeCtr(byte[] input, final byte[] iv, byte[] output, int outputOffset) throws ShortBufferException { + private int computeCtr(byte[] input, int inOff, int inLen, final byte[] iv, int ivOff, int ivLen, byte[] output, int outputOffset) throws ShortBufferException { // clear out the 31st and 63rd (rightmost) bit: - final byte[] adjustedIv = Arrays.copyOf(iv, 16); + assert ivLen == IV_LENGTH; + final byte[] adjustedIv = Arrays.copyOfRange(iv, ivOff, ivOff + ivLen); adjustedIv[8] = (byte) (adjustedIv[8] & 0x7F); adjustedIv[12] = (byte) (adjustedIv[12] & 0x7F); try { ctrCipher.init(Cipher.ENCRYPT_MODE, ctrKey, new IvParameterSpec(adjustedIv)); - return ctrCipher.doFinal(input, 0, input.length, output, outputOffset); + return ctrCipher.doFinal(input, inOff, inLen, output, outputOffset); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { throw new IllegalArgumentException("Key or IV invalid."); } catch (BadPaddingException e) { From 68fa9a0153d2c2f6d6161042521b9079f3bffc42 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 09:19:57 +0100 Subject: [PATCH 13/25] bump version to 2.0.0-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d656336..228c650 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.cryptomator siv-mode - 1.7.0-SNAPSHOT + 2.0.0-SNAPSHOT SIV Mode RFC 5297 SIV mode: deterministic authenticated encryption From 0612845bdfd537859968da1757cdf7462807f9f2 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 09:37:53 +0100 Subject: [PATCH 14/25] new build workflow [deploy] --- .github/workflows/build.yml | 108 ++++++++++++++++++++++---- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/publish-central.yml | 30 ------- .github/workflows/publish-github.yml | 26 ------- 4 files changed, 95 insertions(+), 71 deletions(-) delete mode 100644 .github/workflows/publish-central.yml delete mode 100644 .github/workflows/publish-github.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 947da3f..29614e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,18 +3,25 @@ on: push: pull_request_target: types: [labeled] + +env: + JAVA_VERSION: 21 + jobs: build: name: Build and Test runs-on: ubuntu-latest + permissions: + id-token: write # Required for the attestations step + attestations: write # Required for the attestations step + outputs: + sha256: ${{ steps.checksums.outputs.sha256 }} steps: - uses: actions/checkout@v5 - with: - fetch-depth: 0 - uses: actions/setup-java@v5 with: - java-version: 21 - distribution: 'zulu' + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} cache: 'maven' - name: Cache SonarCloud packages uses: actions/cache@v4 @@ -24,10 +31,10 @@ jobs: restore-keys: ${{ runner.os }}-sonar - name: Ensure to use tagged version if: startsWith(github.ref, 'refs/tags/') - run: ./mvnw -B versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} + run: ./mvnw versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} - name: Build and Test run: > - ./mvnw -B verify + ./mvnw -B verify --no-transfer-progress jacoco:report org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Pcoverage @@ -37,10 +44,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - uses: actions/upload-artifact@v5 - with: - name: artifacts - path: target/*.jar - name: Calculate Checksums id: checksums run: | @@ -49,11 +52,89 @@ jobs: shasum -a256 target/*.jar echo EOF } >> $GITHUB_OUTPUT - - name: Create Release + - name: Attest if: startsWith(github.ref, 'refs/tags/') + uses: actions/attest-build-provenance@v3 + with: + subject-path: | + target/*.jar + target/*.pom + - uses: actions/upload-artifact@v5 + with: + name: artifacts + path: target/*.jar + + deploy-central: + name: Deploy to Maven Central + runs-on: ubuntu-latest + permissions: {} + needs: [build] + if: github.repository_owner == 'cryptomator' && (startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[deploy]')) + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + server-id: central + server-username: MAVEN_CENTRAL_USERNAME + server-password: MAVEN_CENTRAL_PASSWORD + - name: Verify project version matches tag + if: startsWith(github.ref, 'refs/tags/') + run: | + PROJECT_VERSION=$(./mvnw help:evaluate "-Dexpression=project.version" -q -DforceStdout) + test "$PROJECT_VERSION" = "${GITHUB_REF##*/}" + - name: Deploy to Maven Central + run: ./mvnw deploy -B -DskipTests -Psign,deploy-central --no-transfer-progress + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import + MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} + + deploy-github: + name: Deploy to GitHub Packages + runs-on: ubuntu-latest + permissions: + packages: write # Required for the deploy to GitHub Packages step + needs: [build] + if: github.repository_owner == 'cryptomator' && (startsWith(github.ref, 'refs/tags/') || contains(github.event.head_commit.message, '[deploy]')) + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-java@v5 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: 'maven' + - name: Verify project version matches tag + if: startsWith(github.ref, 'refs/tags/') + run: | + PROJECT_VERSION=$(./mvnw help:evaluate "-Dexpression=project.version" -q -DforceStdout) + test "$PROJECT_VERSION" = "${GITHUB_REF##*/}" + - name: Deploy to GitHub Packages + run: ./mvnw deploy -B -DskipTests -Psign,deploy-github --no-transfer-progress + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import + MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} + + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: write # Required for the release step + needs: [build, deploy-central, deploy-github] + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Create Release uses: softprops/action-gh-release@v2 with: + prerelease: true token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + generate_release_notes: true body: |- ### Maven Coordinates ```xml @@ -66,8 +147,7 @@ jobs: ### Artifact Checksums ```txt - ${{ steps.checksums.outputs.sha256 }} + ${{ needs.build.outputs.sha256 }} ``` - See [README.md](https://github.com/cryptomator/siv-mode/#reproducible-builds) section regarding reproducing this build. - generate_release_notes: true + See [README.md](https://github.com/cryptomator/siv-mode/#reproducible-builds) section regarding reproducing this build. \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1e0f2bf..b93530b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -26,7 +26,7 @@ jobs: uses: actions/setup-java@v5 with: java-version: 21 - distribution: 'zulu' + distribution: 'temurin' cache: 'maven' - name: Initialize CodeQL uses: github/codeql-action/init@v4 diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml deleted file mode 100644 index 211bcfe..0000000 --- a/.github/workflows/publish-central.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Publish to Maven Central -on: - release: - types: [published] -jobs: - publish: - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') # only allow publishing tagged versions - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-java@v5 - with: - java-version: 21 - distribution: 'zulu' - cache: 'maven' - server-id: central - server-username: MAVEN_CENTRAL_USERNAME - server-password: MAVEN_CENTRAL_PASSWORD - - name: Verify project version = ${{ github.event.release.tag_name }} - run: | - PROJECT_VERSION=$(./mvnw help:evaluate "-Dexpression=project.version" -q -DforceStdout) - test "$PROJECT_VERSION" = "${{ github.event.release.tag_name }}" - - name: Deploy - run: ./mvnw deploy -B -DskipTests -Psign,deploy-central --no-transfer-progress - env: - MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} - MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} - MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} \ No newline at end of file diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml deleted file mode 100644 index 04bc4d9..0000000 --- a/.github/workflows/publish-github.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Publish to GitHub Packages -on: - release: - types: [published] -jobs: - publish: - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') # only allow publishing tagged versions - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-java@v5 - with: - java-version: 21 - distribution: 'zulu' - cache: 'maven' - - name: Verify project version = ${{ github.event.release.tag_name }} - run: | - PROJECT_VERSION=$(./mvnw help:evaluate "-Dexpression=project.version" -q -DforceStdout) - test "$PROJECT_VERSION" = "${{ github.event.release.tag_name }}" - - name: Deploy - run: ./mvnw deploy -B -DskipTests -Psign,deploy-github --no-transfer-progress - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} - MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} From 5bcdf0abe84f9ff3198a3f69fba81b1389abf659 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 09:38:34 +0100 Subject: [PATCH 15/25] update README.md [ci skip] --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b41500d..d066a8e 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,9 @@ [![Javadocs](http://www.javadoc.io/badge/org.cryptomator/siv-mode.svg)](http://www.javadoc.io/doc/org.cryptomator/siv-mode) ## Features -- No dependencies (required BouncyCastle classes are repackaged) +- No dependencies - Passes official RFC 5297 test vectors - Constant time authentication -- Defaults on AES, but supports any block cipher with a 128-bit block size. -- Supports any key sizes that the block cipher supports (e.g. 128/192/256-bit keys for AES) - Thread-safe - [Fast](https://github.com/cryptomator/siv-mode/issues/15) - Requires JDK 8+ or Android API Level 24+ (since version 1.4.0) @@ -48,7 +46,7 @@ public void encryptWithAssociatedData() { org.cryptomator siv-mode - 1.4.0 + 2.0.0 ``` @@ -61,8 +59,6 @@ From version 1.3.2 onwards this library is an explicit module with the name `org requires org.cryptomator.siv; ``` -Because BouncyCastle classes are shaded, this library only depends on `java.base`. - ## Reproducible Builds This is a Maven project that can be built using `mvn install`. However, if you want to build this reproducibly, please make sure: From 897420e53f8e586a2f48599f09b9f5cd807e90b2 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 10:30:58 +0100 Subject: [PATCH 16/25] update changelog [ci skip] --- CHANGELOG.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abbf9e9..790a7fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/cryptomator/siv-mode/compare/1.6.0...HEAD) +### Added +- new lowlevel API: + * `new SivEngine(key).encrypt(plaintext, associatedData...)` + * `new SivEngine(key).decrypt(plaintext, associatedData...)` +- implement JCA `Cipher` SPI: + ``` + Cipher siv = Cipher.getInstance("AES/SIV/NoPadding"); + siv.init(Cipher.ENCRYPT_MODE, key); + siv.updateAAD(aad1); + siv.updateAAD(aad2); + byte[] ciphertext = siv.doFinal(plaintext); + ``` + +### Changed +- remove dependencies on BouncyCastle and Jetbrains Annotations +- simplify build by removing `maven-shade-plugin` +- update test dependencies +- update build plugins + +### Deprecated +- old lowlevel API: + * `new SivMode().encrypt(ctrKey, macKey, encrypted)` + * `new SivMode().decrypt(ctrKey, macKey, ciphertext)` + + ## [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 From 6edd8c5c47bc192e661045600cc2336497b6bde6 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 10:31:07 +0100 Subject: [PATCH 17/25] close input stream --- .../cryptomator/siv/EncryptionTestCase.java | 5 + .../org/cryptomator/siv/SivEngineTest.java | 261 ++++++++++-------- 2 files changed, 158 insertions(+), 108 deletions(-) diff --git a/src/test/java/org/cryptomator/siv/EncryptionTestCase.java b/src/test/java/org/cryptomator/siv/EncryptionTestCase.java index 8dfba8e..83227b7 100644 --- a/src/test/java/org/cryptomator/siv/EncryptionTestCase.java +++ b/src/test/java/org/cryptomator/siv/EncryptionTestCase.java @@ -82,4 +82,9 @@ public byte[][] getAssociatedData() { public byte[] getCiphertext() { return Arrays.copyOf(ciphertext, ciphertext.length); } + + @Override + public String toString() { + return "TestCase #" + testCaseNumber; + } } diff --git a/src/test/java/org/cryptomator/siv/SivEngineTest.java b/src/test/java/org/cryptomator/siv/SivEngineTest.java index 4d1ff74..e4a2e79 100644 --- a/src/test/java/org/cryptomator/siv/SivEngineTest.java +++ b/src/test/java/org/cryptomator/siv/SivEngineTest.java @@ -1,12 +1,17 @@ package org.cryptomator.siv; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicContainer; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.FieldSource; import org.junit.jupiter.params.provider.ValueSource; import javax.crypto.AEADBadTagException; @@ -16,9 +21,10 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -293,115 +299,154 @@ public void testSivDecrypt() throws AEADBadTagException, IllegalBlockSizeExcepti } - @TestFactory - public Stream testGeneratedTestCases() { - InputStream in = EncryptionTestCase.class.getResourceAsStream("/testcases.txt"); - Reader reader = new InputStreamReader(in, StandardCharsets.US_ASCII); - BufferedReader bufferedReader = new BufferedReader(reader); - Stream lines = bufferedReader.lines().onClose(() -> { - try { - bufferedReader.close(); - } catch (IOException e) { - throw new UncheckedIOException(e); + @Nested + @DisplayName("Generated Test Cases") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + public class GeneratedTestCases { + + private List testCases; + + @BeforeAll + public void loadTestCases() throws IOException { + try (InputStream in = EncryptionTestCase.class.getResourceAsStream("/testcases.txt"); + Reader reader = new InputStreamReader(in, StandardCharsets.US_ASCII); + BufferedReader bufferedReader = new BufferedReader(reader)) { + this.testCases = bufferedReader.lines().map(EncryptionTestCase::fromLine).collect(Collectors.toList()); } - }); - return lines.map(EncryptionTestCase::fromLine).map(testCase -> { - int testIdx = testCase.getTestCaseNumber(); + } + + @DisplayName("decrypt") + @ParameterizedTest(name = "{0}") + @FieldSource("testCases") + public void testDecrypt(EncryptionTestCase testCase) throws AEADBadTagException, IllegalBlockSizeException { + SivEngine siv = new SivEngine(testCase.getKey()); + byte[] actualPlaintext = siv.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); + Assertions.assertArrayEquals(testCase.getPlaintext(), actualPlaintext); + } + + @DisplayName("encrypt") + @ParameterizedTest(name = "{0}") + @FieldSource("testCases") + public void testEncrypt(EncryptionTestCase testCase) { + SivEngine siv = new SivEngine(testCase.getKey()); + byte[] actualCiphertext = siv.encrypt(testCase.getPlaintext(), testCase.getAssociatedData()); + Assertions.assertArrayEquals(testCase.getCiphertext(), actualCiphertext); + } + + @DisplayName("decrypt fails due to tampered mac key") + @ParameterizedTest(name = "{0}") + @FieldSource("testCases") + public void testDecryptFailsDueToTamperedMacKey(EncryptionTestCase testCase) { + byte[] key = testCase.getKey(); + + // Pick some arbitrary byte from first half of key (i.e. the MAC key) to tamper with + int halfKeyLen = key.length / 2; + int tamperedByteIndex = testCase.getTestCaseNumber() % halfKeyLen; + + // Flip a single bit + key[tamperedByteIndex] ^= 0x10; + + SivEngine sivWithTamperedKey = new SivEngine(key); + + Assertions.assertThrows(AEADBadTagException.class, () -> { + sivWithTamperedKey.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); + }); + } + + @DisplayName("decrypt fails due to tampered ciphertext") + @ParameterizedTest(name = "{0}") + @FieldSource("testCases") + public void testDecryptFailsDueToTamperedCiphertext(EncryptionTestCase testCase) { + SivEngine siv = new SivEngine(testCase.getKey()); + byte[] ciphertext = testCase.getCiphertext(); + + // Pick some arbitrary key byte to tamper with + int tamperedByteIndex = testCase.getTestCaseNumber() % ciphertext.length; + + // Flip a single bit + ciphertext[tamperedByteIndex] ^= 0x10; + + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(ciphertext, testCase.getAssociatedData()); + }); + } + + @DisplayName("decrypt fails due to tampered associated data") + @ParameterizedTest(name = "{0}") + @FieldSource("testCases") + public void testDecryptFailsDueToTamperedAAD(EncryptionTestCase testCase) { + // Skip if there is no AD + if (testCase.getAssociatedData().length == 0) { + return; + } + SivEngine siv = new SivEngine(testCase.getKey()); - return DynamicContainer.dynamicContainer("test case " + testIdx, Arrays.asList( - DynamicTest.dynamicTest("decrypt", () -> { - byte[] actualPlaintext = siv.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); - Assertions.assertArrayEquals(testCase.getPlaintext(), actualPlaintext); - }), - DynamicTest.dynamicTest("encrypt", () -> { - byte[] actualCiphertext = siv.encrypt(testCase.getPlaintext(), testCase.getAssociatedData()); - Assertions.assertArrayEquals(testCase.getCiphertext(), actualCiphertext); - }), - DynamicTest.dynamicTest("decrypt fails due to tampered mac key", () -> { - byte[] key = testCase.getKey(); - - // Pick some arbitrary byte from first half of key (i.e. the MAC key) to tamper with - int halfKeyLen = key.length / 2; - int tamperedByteIndex = testIdx % halfKeyLen; - - // Flip a single bit - key[tamperedByteIndex] ^= 0x10; - - SivEngine sivWithTamperedKey = new SivEngine(key); - - Assertions.assertThrows(AEADBadTagException.class, () -> { - sivWithTamperedKey.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); - }); - }), - DynamicTest.dynamicTest("decrypt fails due to tampered ciphertext", () -> { - byte[] ciphertext = testCase.getCiphertext(); - - // Pick some arbitrary key byte to tamper with - int tamperedByteIndex = testIdx % ciphertext.length; - - // Flip a single bit - ciphertext[tamperedByteIndex] ^= 0x10; - - Assertions.assertThrows(AEADBadTagException.class, () -> { - siv.decrypt(ciphertext, testCase.getAssociatedData()); - }); - }), - DynamicTest.dynamicTest("decrypt fails due to tampered associated data", () -> { - byte[][] ad = testCase.getAssociatedData(); - - // Try flipping bits in the associated data elements - for (int adIdx = 0; adIdx < ad.length; adIdx++) { - // Skip if this ad element is empty - if (ad[adIdx].length == 0) { - continue; - } - - // Pick some arbitrary byte to tamper with - int tamperedByteIndex = testIdx % ad[adIdx].length; - - // Flip a single bit - ad[adIdx][tamperedByteIndex] ^= 0x04; - - Assertions.assertThrows(AEADBadTagException.class, () -> { - siv.decrypt(testCase.getCiphertext(), ad); - }); - - // Restore ad to original value - ad[adIdx][tamperedByteIndex] ^= 0x04; - } - }), - DynamicTest.dynamicTest("decrypt fails due to prepended associated data", () -> { - // Skip if there is no more room for additional AD - if (testCase.getAssociatedData().length > 125) { - return; - } - - byte[][] ad = testCase.getAssociatedData(); - byte[][] prependedAd = new byte[ad.length + 1][]; - prependedAd[0] = new byte[testIdx % 16]; - System.arraycopy(ad, 0, prependedAd, 1, ad.length); - - Assertions.assertThrows(AEADBadTagException.class, () -> { - siv.decrypt(testCase.getCiphertext(), prependedAd); - }); - }), - DynamicTest.dynamicTest("decrypt fails due to appended associated data", () -> { - // Skip if there is no more room for additional AD - if (testCase.getAssociatedData().length > 125) { - return; - } - - byte[][] ad = testCase.getAssociatedData(); - byte[][] appendedAd = new byte[ad.length + 1][]; - appendedAd[ad.length] = new byte[testIdx % 16]; - System.arraycopy(ad, 0, appendedAd, 0, ad.length); - - Assertions.assertThrows(AEADBadTagException.class, () -> { - siv.decrypt(testCase.getCiphertext(), appendedAd); - }); - }) - )); - }); + byte[][] ad = testCase.getAssociatedData(); + + // Try flipping bits in the associated data elements + for (int adIdx = 0; adIdx < ad.length; adIdx++) { + // Skip if this ad element is empty + if (ad[adIdx].length == 0) { + continue; + } + + // Pick some arbitrary byte to tamper with + int tamperedByteIndex = testCase.getTestCaseNumber() % ad[adIdx].length; + + // Flip a single bit + ad[adIdx][tamperedByteIndex] ^= 0x04; + + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(testCase.getCiphertext(), ad); + }); + + // Restore ad to original value + ad[adIdx][tamperedByteIndex] ^= 0x04; + } + } + + @DisplayName("decrypt fails due to prepended associated data") + @ParameterizedTest(name = "{0}") + @FieldSource("testCases") + public void testDecryptFailsDueToPrependedAAD(EncryptionTestCase testCase) { + // Skip if there is no more room for additional AD + if (testCase.getAssociatedData().length > 125) { + return; + } + + SivEngine siv = new SivEngine(testCase.getKey()); + + byte[][] ad = testCase.getAssociatedData(); + byte[][] prependedAd = new byte[ad.length + 1][]; + prependedAd[0] = new byte[testCase.getTestCaseNumber() % 16]; + System.arraycopy(ad, 0, prependedAd, 1, ad.length); + + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(testCase.getCiphertext(), prependedAd); + }); + } + + @DisplayName("decrypt fails due to appended associated data") + @ParameterizedTest(name = "{0}") + @FieldSource("testCases") + public void testDecryptFailsDueToAppendedAAD(EncryptionTestCase testCase) { + // Skip if there is no more room for additional AD + if (testCase.getAssociatedData().length > 125) { + return; + } + + SivEngine siv = new SivEngine(testCase.getKey()); + + byte[][] ad = testCase.getAssociatedData(); + byte[][] appendedAd = new byte[ad.length + 1][]; + appendedAd[ad.length] = new byte[testCase.getTestCaseNumber() % 16]; + System.arraycopy(ad, 0, appendedAd, 0, ad.length); + + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(testCase.getCiphertext(), appendedAd); + }); + } + } } From 4da3f204cc517292b9282529ae3edbb8102572ff Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 11:29:56 +0100 Subject: [PATCH 18/25] deprecate 1.x `SivMode` API --- .../java/org/cryptomator/siv/SivMode.java | 159 ++++++++++++++++++ .../siv/UnauthenticCiphertextException.java | 21 +++ .../java/org/cryptomator/siv/SivModeTest.java | 36 ++++ 3 files changed, 216 insertions(+) create mode 100644 src/main/java/org/cryptomator/siv/SivMode.java create mode 100644 src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java create mode 100644 src/test/java/org/cryptomator/siv/SivModeTest.java diff --git a/src/main/java/org/cryptomator/siv/SivMode.java b/src/main/java/org/cryptomator/siv/SivMode.java new file mode 100644 index 0000000..d049d73 --- /dev/null +++ b/src/main/java/org/cryptomator/siv/SivMode.java @@ -0,0 +1,159 @@ +package org.cryptomator.siv; + +import javax.crypto.AEADBadTagException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.SecretKey; +import java.util.Arrays; + +/** + * Implements the RFC 5297 SIV mode. + */ +@Deprecated +public final class SivMode { + + /** + * Creates an AES-SIV instance using JCE's cipher implementation, which should normally be the best choice.
+ */ + public SivMode() { + } + + /** + * 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 == null) { + throw new IllegalArgumentException("Can't get bytes of given key."); + } + try { + return new SivEngine(keyBytes).encrypt(plaintext, associatedData); + } finally { + 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 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. + * @throws IllegalArgumentException if keys are invalid or {@link SecretKey#getEncoded()} is not supported. + */ + public byte[] encrypt(SecretKey ctrKey, SecretKey macKey, byte[] plaintext, byte[]... associatedData) { + final byte[] ctrKeyBytes = ctrKey.getEncoded(); + final byte[] macKeyBytes = macKey.getEncoded(); + if (ctrKeyBytes == null || macKeyBytes == null) { + throw new IllegalArgumentException("Can't get bytes of given key."); + } + try { + return encrypt(ctrKeyBytes, macKeyBytes, plaintext, associatedData); + } finally { + Arrays.fill(ctrKeyBytes, (byte) 0); + Arrays.fill(macKeyBytes, (byte) 0); + } + } + + /** + * 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 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. + * @throws IllegalArgumentException if the either of the two keys is of invalid length for the used {@link BlockCipher}. + */ + public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... associatedData) { + byte[] combinedKey = new byte[ctrKey.length + macKey.length]; + try { + System.arraycopy(macKey, 0, combinedKey, 0, macKey.length); + System.arraycopy(ctrKey, 0, combinedKey, macKey.length, ctrKey.length); + return new SivEngine(combinedKey).encrypt(plaintext, associatedData); + } finally { + Arrays.fill(combinedKey, (byte) 0); + } + } + + /** + * 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 == null) { + throw new IllegalArgumentException("Can't get bytes of given key."); + } + try { + return new SivEngine(keyBytes).decrypt(ciphertext, associatedData); + } catch (AEADBadTagException e) { + throw new UnauthenticCiphertextException("authentication in SIV decryption failed"); + } finally { + 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 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. + * @throws IllegalArgumentException If keys are invalid or {@link SecretKey#getEncoded()} is not supported. + * @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 ctrKey, SecretKey macKey, byte[] ciphertext, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException { + final byte[] ctrKeyBytes = ctrKey.getEncoded(); + final byte[] macKeyBytes = macKey.getEncoded(); + if (ctrKeyBytes == null || macKeyBytes == null) { + throw new IllegalArgumentException("Can't get bytes of given key."); + } + try { + return decrypt(ctrKeyBytes, macKeyBytes, ciphertext, associatedData); + } finally { + Arrays.fill(ctrKeyBytes, (byte) 0); + Arrays.fill(macKeyBytes, (byte) 0); + } + } + + /** + * 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 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. + * @throws IllegalArgumentException If the either of the two keys is of invalid length. + * @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(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException { + byte[] combinedKey = new byte[ctrKey.length + macKey.length]; + try { + System.arraycopy(macKey, 0, combinedKey, 0, macKey.length); + System.arraycopy(ctrKey, 0, combinedKey, macKey.length, ctrKey.length); + return new SivEngine(combinedKey).decrypt(ciphertext, associatedData); + } catch (AEADBadTagException e) { + throw new UnauthenticCiphertextException("authentication in SIV decryption failed"); + } finally { + Arrays.fill(combinedKey, (byte) 0); + } + } + +} diff --git a/src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java b/src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java new file mode 100644 index 0000000..3964c3c --- /dev/null +++ b/src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java @@ -0,0 +1,21 @@ +package org.cryptomator.siv; + +import javax.crypto.BadPaddingException; + +/** + * Drop-in replacement for {@link javax.crypto.AEADBadTagException}, which is not available on some older Android systems. + */ +@Deprecated +public class UnauthenticCiphertextException extends BadPaddingException { + + /** + * Constructs a UnauthenticCiphertextException with the specified + * detail message. + * + * @param message the detail message. + */ + public UnauthenticCiphertextException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/siv/SivModeTest.java b/src/test/java/org/cryptomator/siv/SivModeTest.java new file mode 100644 index 0000000..ae648c1 --- /dev/null +++ b/src/test/java/org/cryptomator/siv/SivModeTest.java @@ -0,0 +1,36 @@ +package org.cryptomator.siv; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.spec.SecretKeySpec; + +@Deprecated +@DisplayName("test deprecated SivMode API") +class SivModeTest { + + @Test + @DisplayName("encrypt/decrypt with 512 bit key") + public void testEncryptAndDecryptWithSingleKey() throws UnauthenticCiphertextException, IllegalBlockSizeException { + byte[] key = new byte[64]; + byte[] plaintext = "Hello, World!".getBytes(); + byte[] aad = "AdditionalData".getBytes(); + byte[] encrypted = new SivMode().encrypt(new SecretKeySpec(key, "AES"), plaintext, aad); + byte[] decrypted = new SivMode().decrypt(new SecretKeySpec(key, "AES"), encrypted, aad); + Assertions.assertArrayEquals(plaintext, decrypted); + } + + @Test + @DisplayName("encrypt/decrypt with two 256 bit keys") + public void testEncryptAndDecryptWithSeparateKeys() throws UnauthenticCiphertextException, IllegalBlockSizeException { + byte[] key = new byte[32]; + byte[] plaintext = "Hello, World!".getBytes(); + byte[] aad = "AdditionalData".getBytes(); + byte[] encrypted = new SivMode().encrypt(new SecretKeySpec(key, "AES"), new SecretKeySpec(key, "AES"), plaintext, aad); + byte[] decrypted = new SivMode().decrypt(new SecretKeySpec(key, "AES"), new SecretKeySpec(key, "AES"), encrypted, aad); + Assertions.assertArrayEquals(plaintext, decrypted); + } + +} \ No newline at end of file From f05b4d74cab15c0b08442824a61132e1c8291748 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 11:30:14 +0100 Subject: [PATCH 19/25] cleanup [deploy] --- .github/workflows/build.yml | 3 +++ CHANGELOG.md | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 29614e4..3389c43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -136,6 +136,9 @@ jobs: token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} generate_release_notes: true body: |- + ### Full Changelog + See [CHANGELOG.md](https://github.com/cryptomator/siv-mode/blob/develop/CHANGELOG.md). + ### Maven Coordinates ```xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 790a7fe..676d076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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). -## [Unreleased](https://github.com/cryptomator/siv-mode/compare/1.6.0...HEAD) +## [Unreleased](https://github.com/cryptomator/siv-mode/compare/1.6.1...HEAD) ### Added - new lowlevel API: @@ -30,7 +30,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - old lowlevel API: * `new SivMode().encrypt(ctrKey, macKey, encrypted)` * `new SivMode().decrypt(ctrKey, macKey, ciphertext)` - + + +## [1.6.1](https://github.com/cryptomator/siv-mode/compare/1.6.0...1.6.1) + +### Changed +- update dependencies ## [1.6.0](https://github.com/cryptomator/siv-mode/compare/1.5.2...1.6.0) From 513c4ff32698974295a47dfc6979f9c507f405b1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 12:26:51 +0100 Subject: [PATCH 20/25] fix javadoc issues [deploy] --- src/main/java/org/cryptomator/siv/CMac.java | 7 ++++++ .../java/org/cryptomator/siv/SivCipher.java | 5 ++++ .../java/org/cryptomator/siv/SivEngine.java | 23 ++++++++++++++++--- .../java/org/cryptomator/siv/SivMode.java | 2 +- .../java/org/cryptomator/siv/SivProvider.java | 17 +++++++++++++- src/main/java/org/cryptomator/siv/Utils.java | 5 ++++ .../org/cryptomator/siv/package-info.java | 10 +++++--- 7 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java index a9a4434..9f4ee6f 100644 --- a/src/main/java/org/cryptomator/siv/CMac.java +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -157,6 +157,9 @@ private static void encryptBlock(Cipher cipher, byte[] block, byte[] output) { /** * Create a new CMAC instance for incremental message processing + * @param key The AES key (16, 24, or 32 bytes) + * @return The CMAC instance + * @throws IllegalArgumentException if the key length is invalid */ public static CMac create(byte[] key) { if (key.length != 16 && key.length != 24 && key.length != 32) { @@ -175,6 +178,10 @@ public static CMac create(byte[] key) { /** * One-shot CMAC computation + * @param key The AES key (16, 24, or 32 bytes) + * @param message The message to authenticate + * @return The CMAC tag (always {@value BLOCK_SIZE} bytes) + * @throws IllegalArgumentException if the key length is invalid */ public static byte[] tag(byte[] key, byte[] message) { CMac cmac = create(key); diff --git a/src/main/java/org/cryptomator/siv/SivCipher.java b/src/main/java/org/cryptomator/siv/SivCipher.java index 74cdd46..207cff4 100644 --- a/src/main/java/org/cryptomator/siv/SivCipher.java +++ b/src/main/java/org/cryptomator/siv/SivCipher.java @@ -18,6 +18,11 @@ import java.util.Arrays; import java.util.List; +/** + * JCE Cipher implementation for AES-SIV mode. + *

+ * This cipher implements the Synthetic Initialization Vector (SIV) mode as specified in RFC 5297. + */ public class SivCipher extends CipherSpi { private static final byte[] EMPTY = new byte[0]; diff --git a/src/main/java/org/cryptomator/siv/SivEngine.java b/src/main/java/org/cryptomator/siv/SivEngine.java index 773ab3b..35fb5e2 100644 --- a/src/main/java/org/cryptomator/siv/SivEngine.java +++ b/src/main/java/org/cryptomator/siv/SivEngine.java @@ -21,6 +21,11 @@ /** * Implements the RFC 5297 SIV mode. + *

+ * Note: Instances of this class are not thread-safe. + * + * @see RFC 5297 + * @since 2.0 */ public final class SivEngine { @@ -86,6 +91,17 @@ public byte[] encrypt(byte[] plaintext, byte[]... associatedData) { return ciphertext; } + /** + * Encrypts plaintext using SIV mode and writes the result to the provided output buffer. + * + * @param plaintext Your plaintext, which shall be encrypted. + * @param output The output buffer to write IV + ciphertext to. + * @param outputOffset The offset in the output buffer to start writing at. + * @param associatedData Optional associated data, which gets authenticated but not encrypted. + * @return The number of bytes written to the output buffer (should always be {@value IV_LENGTH} + plaintext length). + * @throws ShortBufferException If the output buffer is too small. + * @throws IllegalArgumentException if either param exceeds the limits for safe use. + */ public int encrypt(byte[] plaintext, byte[] output, int outputOffset, byte[]... associatedData) throws ShortBufferException { // Check if plaintext length will cause overflows if (plaintext.length > (Integer.MAX_VALUE - IV_LENGTH)) { @@ -104,11 +120,12 @@ public int encrypt(byte[] plaintext, byte[] output, int outputOffset, byte[]... /** * Decrypts ciphertext using SIV mode. A block cipher defined by the constructor is being used.
* - * @param ciphertext Your ciphertext, which shall be encrypted. + * @param ciphertext Your ciphertext, which shall be decrypted. * @param associatedData Optional associated data, which needs to be authenticated during decryption. * @return Plaintext byte array. - * @throws AEADBadTagException If the authentication failed, e.g. because ciphertext and/or associatedData are corrupted. - * @throws IllegalBlockSizeException If the provided ciphertext is of invalid length. + * @throws AEADBadTagException If the authentication failed, e.g. because ciphertext and/or associatedData are corrupted. + * @throws IllegalBlockSizeException If the provided ciphertext is shorter than 16 bytes. + * @throws IllegalArgumentException If number of associatedData fields exceed the limits for safe use. */ public byte[] decrypt(byte[] ciphertext, byte[]... associatedData) throws AEADBadTagException, IllegalBlockSizeException { if (ciphertext.length < IV_LENGTH) { diff --git a/src/main/java/org/cryptomator/siv/SivMode.java b/src/main/java/org/cryptomator/siv/SivMode.java index d049d73..aa2920f 100644 --- a/src/main/java/org/cryptomator/siv/SivMode.java +++ b/src/main/java/org/cryptomator/siv/SivMode.java @@ -68,7 +68,7 @@ public byte[] encrypt(SecretKey ctrKey, SecretKey macKey, byte[] plaintext, byte * @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. - * @throws IllegalArgumentException if the either of the two keys is of invalid length for the used {@link BlockCipher}. + * @throws IllegalArgumentException if the either of the two keys is of invalid length. */ public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... associatedData) { byte[] combinedKey = new byte[ctrKey.length + macKey.length]; diff --git a/src/main/java/org/cryptomator/siv/SivProvider.java b/src/main/java/org/cryptomator/siv/SivProvider.java index 999898f..b60813a 100644 --- a/src/main/java/org/cryptomator/siv/SivProvider.java +++ b/src/main/java/org/cryptomator/siv/SivProvider.java @@ -3,12 +3,27 @@ import java.security.Provider; import java.util.HashMap; +/** + * JCE Security Provider for AES-SIV mode. + *

+ * Provides implementations for: + *

    + *
  • CMAC (Cipher-based Message Authentication Code)
  • + *
  • AES/SIV/NoPadding cipher
  • + *
+ */ public class SivProvider extends Provider { + /** + * Singleton instance of the SIV provider. + */ public static final SivProvider INSTANCE = new SivProvider(); + /** + * Constructs a new SIV provider and registers the available algorithms. + */ public SivProvider() { - super("SIV", 2.0, ""); + super("SIV", 2.0, "AES-SIV mode provider for authenticated encryption"); putService(new Service(this, "Mac", "CMAC", CMac.class.getName(), null, new HashMap<>())); putService(new Service(this, "Cipher", "AES/SIV/NoPadding", SivCipher.class.getName(), null, new HashMap<>())); } diff --git a/src/main/java/org/cryptomator/siv/Utils.java b/src/main/java/org/cryptomator/siv/Utils.java index e7092ce..fbd6905 100644 --- a/src/main/java/org/cryptomator/siv/Utils.java +++ b/src/main/java/org/cryptomator/siv/Utils.java @@ -2,6 +2,11 @@ import java.util.Arrays; +/** + * Utility methods for cryptographic operations. + *

+ * Provides bit manipulation and padding operations used in AES-SIV mode. + */ public class Utils { private static final byte DOUBLING_CONST = (byte) 0x87; diff --git a/src/main/java/org/cryptomator/siv/package-info.java b/src/main/java/org/cryptomator/siv/package-info.java index bb9e864..e741974 100644 --- a/src/main/java/org/cryptomator/siv/package-info.java +++ b/src/main/java/org/cryptomator/siv/package-info.java @@ -1,8 +1,12 @@ /** * Java implementation of RFC 5297 SIV Authenticated Encryption. *

- * Use an instance of the {@link org.cryptomator.siv.SivEngine} class to - * {@link org.cryptomator.siv.SivEngine#encrypt(byte[], byte[]...) encrypt} or - * {@link org.cryptomator.siv.SivEngine#decrypt(byte[], byte[]...) decrypt} data. + * Two usage patterns are supported: + *

    + *
  • Direct API: Use {@link org.cryptomator.siv.SivEngine} for encrypt/decrypt operations
  • + *
  • JCE Provider: Register {@link org.cryptomator.siv.SivProvider} and use standard JCE APIs
  • + *
+ * + * @see RFC 5297 */ package org.cryptomator.siv; \ No newline at end of file From 6d73307c45269c8775ff53315cc3f0930d8b73fe Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 12:52:14 +0100 Subject: [PATCH 21/25] show migration path in deprecation notice --- src/main/java/org/cryptomator/siv/SivMode.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/org/cryptomator/siv/SivMode.java b/src/main/java/org/cryptomator/siv/SivMode.java index aa2920f..bc2f3ba 100644 --- a/src/main/java/org/cryptomator/siv/SivMode.java +++ b/src/main/java/org/cryptomator/siv/SivMode.java @@ -7,6 +7,8 @@ /** * Implements the RFC 5297 SIV mode. + * + * @deprecated Use {@link SivEngine} instead. */ @Deprecated public final class SivMode { @@ -23,7 +25,9 @@ public SivMode() { * @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. + * @deprecated Use {@link SivEngine#encrypt(byte[], byte[]...)} instead. */ + @Deprecated public byte[] encrypt(SecretKey key, byte[] plaintext, byte[]... associatedData) { final byte[] keyBytes = key.getEncoded(); if (keyBytes == null) { @@ -45,7 +49,9 @@ public byte[] encrypt(SecretKey key, byte[] plaintext, byte[]... associatedData) * @param associatedData Optional associated data, which gets authenticated but not encrypted. * @return IV + Ciphertext as a concatenated byte array. * @throws IllegalArgumentException if keys are invalid or {@link SecretKey#getEncoded()} is not supported. + * @deprecated Use {@link SivEngine#encrypt(byte[], byte[]...)} instead. */ + @Deprecated public byte[] encrypt(SecretKey ctrKey, SecretKey macKey, byte[] plaintext, byte[]... associatedData) { final byte[] ctrKeyBytes = ctrKey.getEncoded(); final byte[] macKeyBytes = macKey.getEncoded(); @@ -69,7 +75,9 @@ public byte[] encrypt(SecretKey ctrKey, SecretKey macKey, byte[] plaintext, byte * @param associatedData Optional associated data, which gets authenticated but not encrypted. * @return IV + Ciphertext as a concatenated byte array. * @throws IllegalArgumentException if the either of the two keys is of invalid length. + * @deprecated Use {@link SivEngine#encrypt(byte[], byte[]...)} instead. */ + @Deprecated public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... associatedData) { byte[] combinedKey = new byte[ctrKey.length + macKey.length]; try { @@ -90,7 +98,9 @@ public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... * @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. + * @deprecated Use {@link SivEngine#decrypt(byte[], byte[]...)} instead. */ + @Deprecated public byte[] decrypt(SecretKey key, byte[] ciphertext, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException { final byte[] keyBytes = key.getEncoded(); if (keyBytes == null) { @@ -116,7 +126,9 @@ public byte[] decrypt(SecretKey key, byte[] ciphertext, byte[]... associatedData * @throws IllegalArgumentException If keys are invalid or {@link SecretKey#getEncoded()} is not supported. * @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. + * @deprecated Use {@link SivEngine#decrypt(byte[], byte[]...)} instead. */ + @Deprecated public byte[] decrypt(SecretKey ctrKey, SecretKey macKey, byte[] ciphertext, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException { final byte[] ctrKeyBytes = ctrKey.getEncoded(); final byte[] macKeyBytes = macKey.getEncoded(); @@ -142,7 +154,9 @@ public byte[] decrypt(SecretKey ctrKey, SecretKey macKey, byte[] ciphertext, byt * @throws IllegalArgumentException If the either of the two keys is of invalid length. * @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. + * @deprecated Use {@link SivEngine#decrypt(byte[], byte[]...)} instead. */ + @Deprecated public byte[] decrypt(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]... associatedData) throws UnauthenticCiphertextException, IllegalBlockSizeException { byte[] combinedKey = new byte[ctrKey.length + macKey.length]; try { From d42ca0529fcfea48cf8f3bc23ab008b99f17436e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 12:55:31 +0100 Subject: [PATCH 22/25] update changelog --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 676d076..75abd62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,8 +28,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated - old lowlevel API: - * `new SivMode().encrypt(ctrKey, macKey, encrypted)` - * `new SivMode().decrypt(ctrKey, macKey, ciphertext)` + * `new SivMode().encrypt(key, plaintext, associatedData...)` + * `new SivMode().encrypt(ctrKey, macKey, plaintext, associatedData...)` + * `new SivMode().decrypt(key, ciphertext, associatedData...)` + * `new SivMode().decrypt(ctrKey, macKey, ciphertext, associatedData...)` + ## [1.6.1](https://github.com/cryptomator/siv-mode/compare/1.6.0...1.6.1) From afd2cd2f7d0874836b39989e524d7651b3e9ff76 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 13:35:52 +0100 Subject: [PATCH 23/25] add tests --- .../org/cryptomator/siv/SivCipherTest.java | 135 ++++++++++++++++++ .../org/cryptomator/siv/SivEngineTest.java | 13 ++ 2 files changed, 148 insertions(+) create mode 100644 src/test/java/org/cryptomator/siv/SivCipherTest.java diff --git a/src/test/java/org/cryptomator/siv/SivCipherTest.java b/src/test/java/org/cryptomator/siv/SivCipherTest.java new file mode 100644 index 0000000..3955f6f --- /dev/null +++ b/src/test/java/org/cryptomator/siv/SivCipherTest.java @@ -0,0 +1,135 @@ +package org.cryptomator.siv; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +class SivCipherTest { + + private SivCipher cipher; + private SecretKey key; + + @BeforeEach + public void setUp() { + this.cipher = new SivCipher(); + this.key = new SecretKeySpec(new byte[64], "AES"); + } + + @Test + public void testEngineGetBlockSize() { + Assertions.assertEquals(16, cipher.engineGetBlockSize()); + } + + @ParameterizedTest + @ValueSource(strings = {"SIV", "siv", "sIv"}) + public void testEngineSetModeValid(String mode) { + Assertions.assertDoesNotThrow(() -> cipher.engineSetMode(mode)); + } + + @ParameterizedTest + @ValueSource(strings = {"CBC", "GCM", "invalid"}) + public void testEngineSetModeInvalid(String mode) { + Assertions.assertThrows(NoSuchAlgorithmException.class, () -> cipher.engineSetMode(mode)); + } + + @ParameterizedTest + @ValueSource(strings = {"NoPadding", "nopadding"}) + public void testEngineSetPaddingValid(String padding) { + Assertions.assertDoesNotThrow(() -> cipher.engineSetPadding(padding)); + } + + @ParameterizedTest + @ValueSource(strings = {"PKCS5Padding", "PKCS7Padding", "invalid"}) + public void testEngineSetPaddingInvalid(String padding) { + Assertions.assertThrows(NoSuchPaddingException.class, () -> cipher.engineSetPadding(padding)); + } + + @Test + public void testEngineGetIV() { + Assertions.assertNull(cipher.engineGetIV()); + } + + @Test + public void testEngineGetParameters() { + Assertions.assertNull(cipher.engineGetParameters()); + } + + @ParameterizedTest + @ValueSource(ints = {32, 48, 64}) + public void testEngineInitWithValidKeySize(int keysize) { + SecretKeySpec key = new SecretKeySpec(new byte[keysize], "AES"); + Assertions.assertDoesNotThrow(() -> cipher.engineInit(Cipher.ENCRYPT_MODE, key, null)); + } + + @ParameterizedTest + @ValueSource(ints = {16, 24, 1337}) + public void testEngineInitWithInvalidKeySize(int keysize) { + SecretKeySpec key = new SecretKeySpec(new byte[keysize], "AES"); + Assertions.assertThrows(InvalidKeyException.class,() -> cipher.engineInit(Cipher.ENCRYPT_MODE, key, null)); + } + + @Test + public void testEngineInitWithAlgorithmParameterSpec() { + Assertions.assertDoesNotThrow(() -> cipher.engineInit(Cipher.ENCRYPT_MODE, key, (java.security.spec.AlgorithmParameterSpec) null, null)); + } + + @Test + public void testEngineInitWithAlgorithmParameters() { + Assertions.assertDoesNotThrow(() -> cipher.engineInit(Cipher.ENCRYPT_MODE, key, (java.security.AlgorithmParameters) null, null)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 15, 16, 17, 100}) + public void testEngineGetOutputSizeEncryptMode(int inLen) throws InvalidKeyException { + cipher.engineInit(Cipher.ENCRYPT_MODE, key, null); + Assertions.assertEquals(16 + inLen, cipher.engineGetOutputSize(inLen)); + } + + @ParameterizedTest + @ValueSource(ints = {16, 17, 100}) + public void testEngineGetOutputSizeDecryptMode(int inLen) throws InvalidKeyException { + cipher.engineInit(Cipher.DECRYPT_MODE, key, null); + Assertions.assertEquals(inLen - 16, cipher.engineGetOutputSize(inLen)); + } + + @Test + public void testWrapAndUnwrap() throws InvalidKeyException, ShortBufferException, IllegalBlockSizeException, BadPaddingException { + // wrap: + cipher.engineInit(Cipher.WRAP_MODE, key, null); + cipher.engineUpdateAAD(StandardCharsets.UTF_8.encode("aad1")); + cipher.engineUpdateAAD("aad2".getBytes(StandardCharsets.UTF_8), 1, 2); + byte[] wrapped1 = cipher.engineUpdate("hello".getBytes(StandardCharsets.UTF_8), 0, "hello".length()); + byte[] wrapped2 = new byte[0]; + int wrapped2Len = cipher.engineUpdate("world".getBytes(StandardCharsets.UTF_8), 0, "world".length(), wrapped2, 0); + Assertions.assertEquals(0, wrapped1.length); + Assertions.assertEquals(0, wrapped2Len); + byte[] wrapped = cipher.engineDoFinal("!".getBytes(StandardCharsets.UTF_8), 0, 1); + + // unwrap: + cipher.engineInit(Cipher.UNWRAP_MODE, key, null); + cipher.engineUpdateAAD(StandardCharsets.UTF_8.encode("aad1")); + cipher.engineUpdateAAD("aad2".getBytes(StandardCharsets.UTF_8), 1, 2); + byte[] unwrapped1 = cipher.engineUpdate(wrapped, 0, 5); + byte[] unwrapped2 = new byte[0]; + int unwrapped2Len = cipher.engineUpdate(wrapped, 5, 5, unwrapped2, 0); + Assertions.assertEquals(0, unwrapped1.length); + Assertions.assertEquals(0, unwrapped2Len); + byte[] unwrapped = cipher.engineDoFinal(wrapped, 10, wrapped.length - 10); + + // compare: + Assertions.assertEquals("helloworld!", new String(unwrapped, StandardCharsets.UTF_8)); + } +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/siv/SivEngineTest.java b/src/test/java/org/cryptomator/siv/SivEngineTest.java index e4a2e79..79cc1a1 100644 --- a/src/test/java/org/cryptomator/siv/SivEngineTest.java +++ b/src/test/java/org/cryptomator/siv/SivEngineTest.java @@ -16,6 +16,7 @@ import javax.crypto.AEADBadTagException; import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -74,6 +75,18 @@ public void testDecryptAssociatedDataLimit() { siv.decrypt(plaintext, new byte[127][0]); }); } + + @Test + public void testEncryptShortBuffer() { + final byte[] key = new byte[32]; + final byte[] plaintext = new byte[80]; + final byte[] output = new byte[95]; // need at least 96 bytes for ciphertext + + SivEngine siv = new SivEngine(key); + Assertions.assertThrows(ShortBufferException.class, () -> { + siv.encrypt(plaintext, output, 0); + }); + } } @ParameterizedTest From 4546454b3ab99a814e38dcef4fd92d6ac9a5fc6a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 20:34:45 +0100 Subject: [PATCH 24/25] fixed findings from code review [deploy] --- CHANGELOG.md | 34 +++++++------- README.md | 1 - src/main/java/org/cryptomator/siv/CMac.java | 7 ++- .../java/org/cryptomator/siv/SivCipher.java | 32 ++++++++++--- .../java/org/cryptomator/siv/SivEngine.java | 6 +-- src/main/java/org/cryptomator/siv/Utils.java | 4 +- .../java/org/cryptomator/siv/CMacTest.java | 46 +++++++++++++++---- .../org/cryptomator/siv/SivCipherTest.java | 17 ++++++- .../org/cryptomator/siv/SivEngineTest.java | 8 ++++ 9 files changed, 112 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75abd62..f870601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,18 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/cryptomator/siv-mode/compare/1.6.1...HEAD) ### Added -- new lowlevel API: - * `new SivEngine(key).encrypt(plaintext, associatedData...)` - * `new SivEngine(key).decrypt(plaintext, associatedData...)` +- new low-level API: + * `new SivEngine(key).encrypt(plaintext, associatedData...)` + * `new SivEngine(key).decrypt(plaintext, associatedData...)` - implement JCA `Cipher` SPI: - ``` - Cipher siv = Cipher.getInstance("AES/SIV/NoPadding"); - siv.init(Cipher.ENCRYPT_MODE, key); - siv.updateAAD(aad1); - siv.updateAAD(aad2); - byte[] ciphertext = siv.doFinal(plaintext); - ``` - + ```java + Cipher siv = Cipher.getInstance("AES/SIV/NoPadding"); + siv.init(Cipher.ENCRYPT_MODE, key); + siv.updateAAD(aad1); + siv.updateAAD(aad2); + byte[] ciphertext = siv.doFinal(plaintext); + ``` + ### Changed - remove dependencies on BouncyCastle and Jetbrains Annotations - simplify build by removing `maven-shade-plugin` @@ -27,13 +27,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - update build plugins ### Deprecated -- old lowlevel API: - * `new SivMode().encrypt(key, plaintext, associatedData...)` - * `new SivMode().encrypt(ctrKey, macKey, plaintext, associatedData...)` - * `new SivMode().decrypt(key, ciphertext, associatedData...)` - * `new SivMode().decrypt(ctrKey, macKey, ciphertext, associatedData...)` - - +- old low-level API: + * `new SivMode().encrypt(key, plaintext, associatedData...)` + * `new SivMode().encrypt(ctrKey, macKey, plaintext, associatedData...)` + * `new SivMode().decrypt(key, ciphertext, associatedData...)` + * `new SivMode().decrypt(ctrKey, macKey, ciphertext, associatedData...)` ## [1.6.1](https://github.com/cryptomator/siv-mode/compare/1.6.0...1.6.1) diff --git a/README.md b/README.md index d066a8e..ae74a8b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ - No dependencies - Passes official RFC 5297 test vectors - Constant time authentication -- Thread-safe - [Fast](https://github.com/cryptomator/siv-mode/issues/15) - Requires JDK 8+ or Android API Level 24+ (since version 1.4.0) diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java index 9f4ee6f..cc06de2 100644 --- a/src/main/java/org/cryptomator/siv/CMac.java +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -58,11 +58,14 @@ protected void engineInit(Key key, AlgorithmParameterSpec params) throws Invalid try { // L = AES_encrypt(K, const_Zero) encryptBlock(cipher, L, L); - this.k1 = dbl(L).clone(); - this.k2 = dbl(L).clone(); + this.k1 = dbl(L.clone()); + this.k2 = dbl(k1.clone()); } finally { Arrays.fill(L, (byte) 0); } + + // reset state + engineReset(); } @Override diff --git a/src/main/java/org/cryptomator/siv/SivCipher.java b/src/main/java/org/cryptomator/siv/SivCipher.java index 207cff4..abd50e4 100644 --- a/src/main/java/org/cryptomator/siv/SivCipher.java +++ b/src/main/java/org/cryptomator/siv/SivCipher.java @@ -123,10 +123,15 @@ protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] o @Override protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) throws IllegalBlockSizeException, BadPaddingException { - byte[] output = new byte[engineGetOutputSize(inputBuffer.length + inputLen)]; + int outputSize = engineGetOutputSize(inputBuffer.length + inputLen); + if (outputSize < 0) { + throw new IllegalBlockSizeException("Ciphertext too short (must be at least 16 bytes including SIV tag)"); + } + byte[] output = new byte[outputSize]; try { engineDoFinal(input, inputOffset, inputLen, output, 0); } catch (ShortBufferException e) { + // outputSize was calculated before, so this should never happen throw new IllegalStateException(e); } return output; @@ -134,23 +139,38 @@ protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) thro @Override protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + int outputSize = engineGetOutputSize(inputBuffer.length + inputLen); + if (outputSize < 0) { + throw new IllegalBlockSizeException("Ciphertext too short (must be at least 16 bytes including SIV tag)"); + } int availableSpace = output.length - outputOffset; - if (availableSpace < engineGetOutputSize(inputBuffer.length + inputLen)) { + if (availableSpace < outputSize) { throw new ShortBufferException(); } engineUpdate(input, inputOffset, inputLen); + int resultLength; SivEngine siv = new SivEngine(this.key); byte[][] aad = this.aad.toArray(new byte[this.aad.size()][]); if (this.opmode == Cipher.ENCRYPT_MODE || this.opmode == Cipher.WRAP_MODE) { - return siv.encrypt(inputBuffer, output, outputOffset, aad); + resultLength = siv.encrypt(inputBuffer, output, outputOffset, aad); } else if (this.opmode == Cipher.DECRYPT_MODE || this.opmode == Cipher.UNWRAP_MODE) { // for security reasons we can't write into output directly before checking integrity: - byte[] plaintext = siv.decrypt(inputBuffer, aad); - System.arraycopy(plaintext, 0, output, outputOffset, plaintext.length); - return plaintext.length; + byte[] plaintext = new byte[0]; + try { + plaintext = siv.decrypt(inputBuffer, aad); + System.arraycopy(plaintext, 0, output, outputOffset, plaintext.length); + resultLength = plaintext.length; + } finally { + Arrays.fill(plaintext, (byte) 0x00); + } } else { throw new IllegalStateException("Invalid opmode " + this.opmode); } + + // reset internal state: + this.inputBuffer = EMPTY; + this.aad.clear(); + return resultLength; } } diff --git a/src/main/java/org/cryptomator/siv/SivEngine.java b/src/main/java/org/cryptomator/siv/SivEngine.java index 35fb5e2..607fff6 100644 --- a/src/main/java/org/cryptomator/siv/SivEngine.java +++ b/src/main/java/org/cryptomator/siv/SivEngine.java @@ -113,8 +113,8 @@ public int encrypt(byte[] plaintext, byte[] output, int outputOffset, byte[]... } byte[] iv = s2v(plaintext, associatedData); assert iv.length == IV_LENGTH; - System.arraycopy(iv, 0, output, 0, IV_LENGTH); - return IV_LENGTH + computeCtr(plaintext, 0, plaintext.length, iv, 0, IV_LENGTH, output, IV_LENGTH); + System.arraycopy(iv, 0, output, outputOffset, IV_LENGTH); + return IV_LENGTH + computeCtr(plaintext, 0, plaintext.length, iv, 0, IV_LENGTH, output, outputOffset + IV_LENGTH); } /** @@ -213,7 +213,7 @@ byte[] s2v(byte[] plaintext, byte[]... associatedData) throws IllegalArgumentExc return cmac.doFinal(end); } else { // T = dbl(D) xor pad(Sn) - byte[] t = xor(dbl(d), pad(plaintext)); + byte[] t = xor(dbl(d), pad(plaintext, 16)); return cmac.doFinal(t); } } diff --git a/src/main/java/org/cryptomator/siv/Utils.java b/src/main/java/org/cryptomator/siv/Utils.java index fbd6905..bdcec03 100644 --- a/src/main/java/org/cryptomator/siv/Utils.java +++ b/src/main/java/org/cryptomator/siv/Utils.java @@ -12,8 +12,8 @@ public class Utils { private static final byte DOUBLING_CONST = (byte) 0x87; // First bit 1, following bits 0. - static byte[] pad(byte[] in) { - final byte[] result = Arrays.copyOf(in, 16); + static byte[] pad(byte[] in, int desiredLength) { + final byte[] result = Arrays.copyOf(in, desiredLength); result[in.length] = (byte) 0x80; return result; } diff --git a/src/test/java/org/cryptomator/siv/CMacTest.java b/src/test/java/org/cryptomator/siv/CMacTest.java index bbff6ea..351e7d3 100644 --- a/src/test/java/org/cryptomator/siv/CMacTest.java +++ b/src/test/java/org/cryptomator/siv/CMacTest.java @@ -1,11 +1,15 @@ package org.cryptomator.siv; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.converter.ArgumentConversionException; import org.junit.jupiter.params.converter.ConvertWith; import org.junit.jupiter.params.converter.SimpleArgumentConverter; import org.junit.jupiter.params.provider.CsvSource; +import java.nio.charset.StandardCharsets; + import static org.junit.jupiter.api.Assertions.assertEquals; class CMacTest { @@ -22,19 +26,26 @@ protected Object convert(Object source, Class targetType) throws ArgumentConv throw new ArgumentConversionException("Source must be a String"); } String hex = (String) source; - if (hex.isEmpty()) { - return new byte[0]; - } - int len = hex.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) - + Character.digit(hex.charAt(i + 1), 16)); - } - return data; + return hexToBytes(hex); } } + /** + * Convert hex string to byte array + */ + private static byte[] hexToBytes(String hex) { + if (hex.isEmpty()) { + return new byte[0]; + } + int len = hex.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4) + + Character.digit(hex.charAt(i + 1), 16)); + } + return data; + } + /** * Convert byte array to hex string */ @@ -84,4 +95,19 @@ void testNistVectors(String testName, assertEquals(expectedHex, bytesToHex(result), testName + " failed"); } + @Test + public void testReset() { + byte[] key = hexToBytes("2b7e151628aed2a6abf7158809cf4f3c"); + byte[] incorrectInput = "oops, never ment to update with this data!".getBytes(StandardCharsets.UTF_8); + byte[] correctInput = hexToBytes("6bc1bee22e409f96e93d7e117393172a"); + + CMac cmac = CMac.create(key); + cmac.engineUpdate(incorrectInput, 0, incorrectInput.length); + cmac.engineReset(); + cmac.engineUpdate(correctInput, 0, correctInput.length); + byte[] tag = cmac.engineDoFinal(); + + assertEquals("070a16b46b4d4144f79bdd9dd04a287c", bytesToHex(tag)); + } + } \ No newline at end of file diff --git a/src/test/java/org/cryptomator/siv/SivCipherTest.java b/src/test/java/org/cryptomator/siv/SivCipherTest.java index 3955f6f..1b0698f 100644 --- a/src/test/java/org/cryptomator/siv/SivCipherTest.java +++ b/src/test/java/org/cryptomator/siv/SivCipherTest.java @@ -78,7 +78,7 @@ public void testEngineInitWithValidKeySize(int keysize) { @ValueSource(ints = {16, 24, 1337}) public void testEngineInitWithInvalidKeySize(int keysize) { SecretKeySpec key = new SecretKeySpec(new byte[keysize], "AES"); - Assertions.assertThrows(InvalidKeyException.class,() -> cipher.engineInit(Cipher.ENCRYPT_MODE, key, null)); + Assertions.assertThrows(InvalidKeyException.class, () -> cipher.engineInit(Cipher.ENCRYPT_MODE, key, null)); } @Test @@ -132,4 +132,19 @@ public void testWrapAndUnwrap() throws InvalidKeyException, ShortBufferException // compare: Assertions.assertEquals("helloworld!", new String(unwrapped, StandardCharsets.UTF_8)); } + + @Test + public void testReuseCipher() throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + byte[] plaintext = "helloworld".getBytes(StandardCharsets.UTF_8); + cipher.engineInit(Cipher.ENCRYPT_MODE, key, null); + byte[] encrypted1 = cipher.engineDoFinal(plaintext, 0, plaintext.length); + byte[] encrypted2 = cipher.engineDoFinal(plaintext, 0, plaintext.length); + cipher.engineInit(Cipher.DECRYPT_MODE, key, null); + byte[] decrypted1 = cipher.engineDoFinal(encrypted1, 0, encrypted1.length); + byte[] decrypted2 = cipher.engineDoFinal(encrypted2, 0, encrypted2.length); + + Assertions.assertArrayEquals(encrypted1, encrypted2); + Assertions.assertArrayEquals(plaintext, decrypted1); + Assertions.assertArrayEquals(plaintext, decrypted2); + } } \ No newline at end of file diff --git a/src/test/java/org/cryptomator/siv/SivEngineTest.java b/src/test/java/org/cryptomator/siv/SivEngineTest.java index 79cc1a1..438a6fe 100644 --- a/src/test/java/org/cryptomator/siv/SivEngineTest.java +++ b/src/test/java/org/cryptomator/siv/SivEngineTest.java @@ -169,6 +169,14 @@ public void testSivEncrypt() { Assertions.assertArrayEquals(ciphertext, result); } + @Test + public void testSivEncryptWithOffset() throws ShortBufferException { + final byte[] result = new byte[ciphertext.length + 10]; + int encrypted = new SivEngine(key).encrypt(plaintext, result, 10, ad); + Assertions.assertEquals(ciphertext.length, encrypted); + Assertions.assertArrayEquals(ciphertext, Arrays.copyOfRange(result, 10, result.length)); + } + @Test public void testSivDecrypt() throws AEADBadTagException, IllegalBlockSizeException { final byte[] result = new SivEngine(key).decrypt(ciphertext, ad); From a456e7b2254707fc731594e0eb66877896c38943 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Nov 2025 20:53:29 +0100 Subject: [PATCH 25/25] fixed findings from code review [deploy] --- src/main/java/org/cryptomator/siv/CMac.java | 4 +--- src/main/java/org/cryptomator/siv/SivCipher.java | 8 ++++---- src/main/java/org/cryptomator/siv/Utils.java | 3 +++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/cryptomator/siv/CMac.java b/src/main/java/org/cryptomator/siv/CMac.java index cc06de2..a57b538 100644 --- a/src/main/java/org/cryptomator/siv/CMac.java +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -45,6 +45,7 @@ protected int engineGetMacLength() { @Override protected void engineInit(Key key, AlgorithmParameterSpec params) throws InvalidKeyException { + engineReset(); try { this.cipher = Cipher.getInstance(AES_ECB_NO_PADDING); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { @@ -63,9 +64,6 @@ protected void engineInit(Key key, AlgorithmParameterSpec params) throws Invalid } finally { Arrays.fill(L, (byte) 0); } - - // reset state - engineReset(); } @Override diff --git a/src/main/java/org/cryptomator/siv/SivCipher.java b/src/main/java/org/cryptomator/siv/SivCipher.java index abd50e4..afe12d6 100644 --- a/src/main/java/org/cryptomator/siv/SivCipher.java +++ b/src/main/java/org/cryptomator/siv/SivCipher.java @@ -54,9 +54,9 @@ protected int engineGetBlockSize() { @Override protected int engineGetOutputSize(int inputLen) { if (opmode == Cipher.ENCRYPT_MODE || opmode == Cipher.WRAP_MODE) { - return 16 + inputLen; + return 16 + inputBuffer.length + inputLen; } else if (opmode == Cipher.DECRYPT_MODE || opmode == Cipher.UNWRAP_MODE) { - return inputLen - 16; + return inputBuffer.length + inputLen - 16; } else { throw new IllegalStateException("Invalid opmode " + this.opmode); } @@ -123,7 +123,7 @@ protected int engineUpdate(byte[] input, int inputOffset, int inputLen, byte[] o @Override protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) throws IllegalBlockSizeException, BadPaddingException { - int outputSize = engineGetOutputSize(inputBuffer.length + inputLen); + int outputSize = engineGetOutputSize(inputLen); if (outputSize < 0) { throw new IllegalBlockSizeException("Ciphertext too short (must be at least 16 bytes including SIV tag)"); } @@ -139,7 +139,7 @@ protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) thro @Override protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { - int outputSize = engineGetOutputSize(inputBuffer.length + inputLen); + int outputSize = engineGetOutputSize(inputLen); if (outputSize < 0) { throw new IllegalBlockSizeException("Ciphertext too short (must be at least 16 bytes including SIV tag)"); } diff --git a/src/main/java/org/cryptomator/siv/Utils.java b/src/main/java/org/cryptomator/siv/Utils.java index bdcec03..cfa0406 100644 --- a/src/main/java/org/cryptomator/siv/Utils.java +++ b/src/main/java/org/cryptomator/siv/Utils.java @@ -13,6 +13,9 @@ public class Utils { // First bit 1, following bits 0. static byte[] pad(byte[] in, int desiredLength) { + if (in.length >= desiredLength) { + throw new IllegalArgumentException("pad() expects input shorter than desiredLength"); + } final byte[] result = Arrays.copyOf(in, desiredLength); result[in.length] = (byte) 0x80; return result;