diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a731bb..77b62af 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -140,6 +140,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 abbf9e9..f870601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,44 @@ 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 low-level API: + * `new SivEngine(key).encrypt(plaintext, associatedData...)` + * `new SivEngine(key).decrypt(plaintext, associatedData...)` +- implement JCA `Cipher` SPI: + ```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` +- update test dependencies +- update build plugins + +### Deprecated +- 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) + +### Changed +- update dependencies ## [1.6.0](https://github.com/cryptomator/siv-mode/compare/1.5.2...1.6.0) ### Added - - This CHANGELOG file - `encrypt(SecretKey key, byte[] plaintext, byte[]... associatedData)` and `decrypt(SecretKey key, byte[] ciphertext, byte[]... associatedData)` using a single 256, 384, or 512 bit key ### Changed - - use `maven-gpg-plugin`'s bc-based signer diff --git a/README.md b/README.md index 23d89a1..ae74a8b 100644 --- a/README.md +++ b/README.md @@ -8,12 +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) @@ -28,16 +25,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()); } ``` @@ -48,7 +45,7 @@ public void encryptWithAssociatedData() { org.cryptomator siv-mode - 1.4.0 + 2.0.0 ``` @@ -61,8 +58,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: diff --git a/pom.xml b/pom.xml index ff03684..50326c9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,9 +1,10 @@ - + 4.0.0 org.cryptomator siv-mode - 1.7.0-SNAPSHOT + 2.0.0-SNAPSHOT SIV Mode RFC 5297 SIV mode: deterministic authenticated encryption @@ -37,9 +38,6 @@ UTF-8 2025-03-14T12:02:43Z - - 1.82 - 6.0.1 5.20.0 @@ -49,23 +47,12 @@ 12.1.8 + + + - - org.bouncycastle - bcprov-jdk18on - ${bouncycastle.version} - - true - - - org.jetbrains - annotations - 26.0.2-1 - provided - - org.junit.jupiter @@ -134,6 +121,19 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + jar-paths-to-properties + validate + + properties + + + + maven-compiler-plugin 3.14.1 @@ -141,6 +141,13 @@ 8 UTF-8 true + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + @@ -163,6 +170,9 @@ org.apache.maven.plugins maven-surefire-plugin 3.5.4 + + @{surefire.jacoco.args} -javaagent:${org.mockito:mockito-core:jar} + maven-jar-plugin @@ -208,43 +218,6 @@ 8 - - maven-shade-plugin - 3.6.1 - - - 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 +265,9 @@ prepare-agent + + surefire.jacoco.args + @@ -353,7 +329,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..a57b538 --- /dev/null +++ b/src/main/java/org/cryptomator/siv/CMac.java @@ -0,0 +1,192 @@ +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.ShortBufferException; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +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. + */ +public class CMac extends MacSpi { + + 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 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 int msgLen = 0; + + @Override + protected int engineGetMacLength() { + return BLOCK_SIZE; + } + + @Override + protected void engineInit(Key key, AlgorithmParameterSpec params) throws InvalidKeyException { + engineReset(); + 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) + encryptBlock(cipher, L, L); + this.k1 = dbl(L.clone()); + this.k2 = dbl(k1.clone()); + } 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) { + 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); + 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() { + xor(x, buffer, y); // Y := X XOR M_i; + encryptBlock(cipher, y, x); // 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 = new byte[BLOCK_SIZE]; + if (flag) { + // M_last := M_n XOR K1; + xor(buffer, k1, m_last); + } 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 + } + xor(buffer, k2, m_last); + } + + // Step 7: + xor(x, m_last, y); // Y := M_last XOR X; + try { + byte[] t = new byte[BLOCK_SIZE]; + encryptBlock(cipher, y, t); // T := AES-128(K,Y); + return t; + } 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); + } + + private static void encryptBlock(Cipher cipher, byte[] block, byte[] output) { + try { + 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); + } + } + + /** + * 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) { + 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 + * @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); + cmac.engineUpdate(message, 0, message.length); + return cmac.engineDoFinal(); + } +} 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/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/SivCipher.java b/src/main/java/org/cryptomator/siv/SivCipher.java new file mode 100644 index 0000000..afe12d6 --- /dev/null +++ b/src/main/java/org/cryptomator/siv/SivCipher.java @@ -0,0 +1,176 @@ +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; + +/** + * 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]; + + 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 + inputBuffer.length + inputLen; + } else if (opmode == Cipher.DECRYPT_MODE || opmode == Cipher.UNWRAP_MODE) { + return inputBuffer.length + 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 { + int outputSize = engineGetOutputSize(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; + } + + @Override + protected int engineDoFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + int outputSize = engineGetOutputSize(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 < 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) { + 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 = 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 new file mode 100644 index 0000000..607fff6 --- /dev/null +++ b/src/main/java/org/cryptomator/siv/SivEngine.java @@ -0,0 +1,220 @@ +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; +import java.security.InvalidKeyException; +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; + +/** + * 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 { + + private static final int IV_LENGTH = CMac.BLOCK_SIZE; + private static final byte[] BYTES_ZERO = new byte[16]; + + private final SecretKey macKey; + private final SecretKey ctrKey; + + private final Mac cmac; + private final Cipher ctrCipher; + + /** + * Creates an AES-SIV instance using JCE's cipher implementation, which should normally be the best choice. + * + * @param key A 256, 384, or 512 bit key. The first half is used for nonce generation, the second half for encryption + */ + 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."); + } + 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"); + + try { + 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 { + 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 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 param exceeds the limits for safe use. + */ + public byte[] encrypt(byte[] plaintext, byte[]... associatedData) { + final byte[] ciphertext = new byte[IV_LENGTH + plaintext.length]; + try { + int encrypted = encrypt(plaintext, ciphertext, 0, associatedData); + assert encrypted == ciphertext.length; + } catch (ShortBufferException e) { + throw new IllegalStateException(e); + } + 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)) { + throw new IllegalArgumentException("Plaintext is too long"); + } + + if (output.length - outputOffset < IV_LENGTH + plaintext.length) { + throw new ShortBufferException(); + } + byte[] iv = s2v(plaintext, associatedData); + assert iv.length == 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); + } + + /** + * Decrypts ciphertext using SIV mode. A block cipher defined by the constructor is being used.
+ * + * @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 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) { + throw new IllegalBlockSizeException("Input length must be greater than or equal 16."); + } + + 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 control.length == IV_LENGTH; + int diff = 0; + for (int i = 0; i < IV_LENGTH; i++) { + diff |= ciphertext[i] ^ control[i]; + } + + if (diff == 0) { + return plaintext; + } else { + Arrays.fill(plaintext, (byte) 0x00); + 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, 0, input.length, iv, 0, iv.length, output, 0); + assert processed == output.length; + } catch (ShortBufferException e) { + throw new IllegalStateException(e); + } + return output; + } + + 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: + 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, inOff, inLen, output, outputOffset); + } 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); + } + } + + // 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) { + // SIV mode cannot be used safely with this many AD fields + throw new IllegalArgumentException("too many Associated Data fields"); + } + + // 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 = cmac.doFinal(BYTES_ZERO); + + for (byte[] s : associatedData) { + xor(dbl(d), cmac.doFinal(s), d); + } + + if (plaintext.length >= 16) { + // 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 = dbl(D) xor pad(Sn) + byte[] t = xor(dbl(d), pad(plaintext, 16)); + return cmac.doFinal(t); + } + } +} diff --git a/src/main/java/org/cryptomator/siv/SivMode.java b/src/main/java/org/cryptomator/siv/SivMode.java index 3f48c55..bc2f3ba 100644 --- a/src/main/java/org/cryptomator/siv/SivMode.java +++ b/src/main/java/org/cryptomator/siv/SivMode.java @@ -1,103 +1,22 @@ 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.AEADBadTagException; import javax.crypto.IllegalBlockSizeException; import javax.crypto.SecretKey; -import java.security.Provider; import java.util.Arrays; /** * Implements the RFC 5297 SIV mode. + * + * @deprecated Use {@link SivEngine} instead. */ +@Deprecated 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); - } - - /** - * 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)); - } - - /** - * 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. - */ - @FunctionalInterface - interface CtrComputer { - byte[] computeCtr(byte[] input, byte[] key, final byte[] iv); } /** @@ -106,12 +25,18 @@ interface CtrComputer { * @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) { + throw new IllegalArgumentException("Can't get bytes of given key."); + } try { - return deriveSubkeysAndThen(this::encrypt, key, plaintext, associatedData); - } catch (UnauthenticCiphertextException | IllegalBlockSizeException e) { - throw new IllegalStateException("Exceptions only expected during decryption", e); + return new SivEngine(keyBytes).encrypt(plaintext, associatedData); + } finally { + Arrays.fill(keyBytes, (byte) 0); } } @@ -124,12 +49,20 @@ 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(); + if (ctrKeyBytes == null || macKeyBytes == null) { + throw new IllegalArgumentException("Can't get bytes of given key."); + } try { - return getEncodedAndThen(this::encrypt, ctrKey, macKey, plaintext, associatedData); - } catch (UnauthenticCiphertextException | IllegalBlockSizeException e) { - throw new IllegalStateException("Exceptions only expected during decryption", e); + return encrypt(ctrKeyBytes, macKeyBytes, plaintext, associatedData); + } finally { + Arrays.fill(ctrKeyBytes, (byte) 0); + Arrays.fill(macKeyBytes, (byte) 0); } } @@ -141,22 +74,19 @@ 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. + * @deprecated Use {@link SivEngine#encrypt(byte[], byte[]...)} instead. */ + @Deprecated public byte[] encrypt(byte[] ctrKey, byte[] macKey, 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"); + 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); } - - final byte[] iv = s2v(macKey, plaintext, associatedData); - final byte[] ciphertext = computeCtr(plaintext, ctrKey, 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; } /** @@ -168,9 +98,21 @@ 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 { - return deriveSubkeysAndThen(this::decrypt, key, ciphertext, associatedData); + 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); + } } /** @@ -184,9 +126,21 @@ 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 { - return getEncodedAndThen(this::decrypt, ctrKey, macKey, ciphertext, 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 decrypt(ctrKeyBytes, macKeyBytes, ciphertext, associatedData); + } finally { + Arrays.fill(ctrKeyBytes, (byte) 0); + Arrays.fill(macKeyBytes, (byte) 0); + } } /** @@ -197,203 +151,23 @@ 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. + * @deprecated Use {@link SivEngine#decrypt(byte[], byte[]...)} instead. */ + @Deprecated public byte[] decrypt(byte[] ctrKey, byte[] macKey, 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); - - // time-constant comparison (taken from MessageDigest.isEqual in JDK8) - assert iv.length == control.length; - int diff = 0; - for (int i = 0; i < iv.length; i++) { - diff |= iv[i] ^ control[i]; - } - - if (diff == 0) { - return plaintext; - } else { - throw new UnauthenticCiphertextException("authentication in SIV decryption failed"); - } - } - - - /** - * 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."); - } + byte[] combinedKey = new byte[ctrKey.length + macKey.length]; try { - return encryptOrDecrypt.compute(ctrKeyBytes, macKeyBytes, data, associatedData); + 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(ctrKeyBytes, (byte) 0); - Arrays.fill(macKeyBytes, (byte) 0); - } - } - - @VisibleForTesting - byte[] computeCtr(byte[] input, byte[] key, 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); - } - - @VisibleForTesting - byte[] s2v(byte[] macKey, 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 - 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); - - for (byte[] s : associatedData) { - d = xor(dbl(d), mac(mac, s)); - } - - final byte[] t; - if (plaintext.length >= 16) { - t = xorend(plaintext, d); - } else { - t = xor(dbl(d), pad(plaintext)); - } - - return mac(mac, 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; - } - - // 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); - 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]; - for (int i = 0; i < result.length; i++) { - result[i] = (byte) (in1[i] ^ in2[i]); - } - return result; - } - - @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]); + Arrays.fill(combinedKey, (byte) 0); } - return result; } } 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..b60813a --- /dev/null +++ b/src/main/java/org/cryptomator/siv/SivProvider.java @@ -0,0 +1,31 @@ +package org.cryptomator.siv; + +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, "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/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/UnauthenticCiphertextException.java b/src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java index 83dafb9..3964c3c 100644 --- a/src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java +++ b/src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java @@ -5,6 +5,7 @@ /** * Drop-in replacement for {@link javax.crypto.AEADBadTagException}, which is not available on some older Android systems. */ +@Deprecated public class UnauthenticCiphertextException extends BadPaddingException { /** @@ -17,4 +18,4 @@ public UnauthenticCiphertextException(String message) { super(message); } -} +} \ No newline at end of file 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..cfa0406 --- /dev/null +++ b/src/main/java/org/cryptomator/siv/Utils.java @@ -0,0 +1,62 @@ +package org.cryptomator.siv; + +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; + + // 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; + } + + static int shiftLeft(byte[] block, byte[] output) { + 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 carry; + } + + 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; + data[data.length - 1] ^= xor & mask; + + return 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++) { + out[i] = (byte) (in1[i] ^ in2[i]); + } + return out; + } + +} 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/java/org/cryptomator/siv/package-info.java b/src/main/java/org/cryptomator/siv/package-info.java index f3bbc1c..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.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. + * 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 diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java index c8ce74e..4961b89 100644 --- a/src/main/java9/module-info.java +++ b/src/main/java9/module-info.java @@ -1,12 +1,7 @@ -module org.cryptomator.siv { - requires static org.bouncycastle.provider; - requires static org.jetbrains.annotations; +import java.security.Provider; +module org.cryptomator.siv { 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; + + provides Provider with org.cryptomator.siv.SivProvider; } \ 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/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/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..351e7d3 --- /dev/null +++ b/src/test/java/org/cryptomator/siv/CMacTest.java @@ -0,0 +1,113 @@ +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 { + + /** + * 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; + 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 + */ + 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"); + } + + @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/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/EncryptionTestCase.java b/src/test/java/org/cryptomator/siv/EncryptionTestCase.java index a86591e..83227b7 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); } @@ -75,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/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/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/SivCipherTest.java b/src/test/java/org/cryptomator/siv/SivCipherTest.java new file mode 100644 index 0000000..1b0698f --- /dev/null +++ b/src/test/java/org/cryptomator/siv/SivCipherTest.java @@ -0,0 +1,150 @@ +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)); + } + + @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 new file mode 100644 index 0000000..438a6fe --- /dev/null +++ b/src/test/java/org/cryptomator/siv/SivEngineTest.java @@ -0,0 +1,473 @@ +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; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 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 SivEngineTest { + + @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 SivEngine(key)); + } + + @Test + public void testDecryptWithInvalidBlockSize() { + final byte[] key = new byte[32]; + + SivEngine siv = new SivEngine(key); + Assertions.assertThrows(IllegalBlockSizeException.class, () -> { + siv.decrypt(new byte[10]); + }); + } + + @Test + public void testEncryptAssociatedDataLimit() { + final byte[] key = new byte[32]; + final byte[] plaintext = new byte[30]; + + SivEngine siv = new SivEngine(key); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + siv.encrypt(plaintext, new byte[127][0]); + }); + } + + @Test + public void testDecryptAssociatedDataLimit() { + final byte[] key = new byte[32]; + final byte[] plaintext = new byte[80]; + + SivEngine siv = new SivEngine(key); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + 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 + @ValueSource(ints = {32, 48, 64}) + public void testEncryptionAndDecryption(int keylen) throws AEADBadTagException, IllegalBlockSizeException { + final byte[] key = new byte[keylen]; + final SivEngine siv = new SivEngine(key); + final byte[] cleartext = "hello world".getBytes(); + final byte[] ciphertext = siv.encrypt(cleartext); + final byte[] decrypted = siv.decrypt(ciphertext); + Assertions.assertArrayEquals(cleartext, decrypted); + } + + // https://tools.ietf.org/html/rfc5297#appendix-A.1 + @Nested + public class RfcTestVector1 { + + 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, // + + (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}; + + 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}; + + 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[] 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 SivEngine(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 SivEngine(key).s2v(plaintext, ad); + Assertions.assertArrayEquals(expected, result); + } + + @Test + public void testSivEncrypt() { + final byte[] result = new SivEngine(key).encrypt(plaintext, ad); + 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); + Assertions.assertArrayEquals(plaintext, result); + } + + @Test + public void testSivDecryptWithInvalidKey() { + final byte[] invalidKey = Arrays.copyOf(key, key.length); + invalidKey[invalidKey.length - 1] = 0x00; + + SivEngine siv = new SivEngine(invalidKey); + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(ciphertext, ad); + }); + } + + @Test + public void testSivDecryptWithInvalidCiphertext() { + final byte[] invalidCiphertext = Arrays.copyOf(ciphertext, ciphertext.length); + invalidCiphertext[invalidCiphertext.length - 1] = 0x00; + + SivEngine siv = new SivEngine(key); + Assertions.assertThrows(AEADBadTagException.class, () -> { + siv.decrypt(invalidCiphertext); + }); + } + + @Test + public void testSivDecryptWithTruncatedCiphertext() { + final byte[] invalidCiphertext = Arrays.copyOf(ciphertext, 15); + + SivEngine siv = new SivEngine(key); + Assertions.assertThrows(IllegalBlockSizeException.class, () -> { + siv.decrypt(invalidCiphertext); + }); + } + + } + + // https://tools.ietf.org/html/rfc5297#appendix-A.2 + @Nested + public class RfcTestVector2 { + + 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) 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[] ad1 = {(byte) 0x00, (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, (byte) 0xff, // + (byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, // + (byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, // + (byte) 0xff, (byte) 0xee, (byte) 0xdd, (byte) 0xcc, // + (byte) 0xbb, (byte) 0xaa, (byte) 0x99, (byte) 0x88, // + (byte) 0x77, (byte) 0x66, (byte) 0x55, (byte) 0x44, // + (byte) 0x33, (byte) 0x22, (byte) 0x11, (byte) 0x00}; + + final byte[] ad2 = {(byte) 0x10, (byte) 0x20, (byte) 0x30, (byte) 0x40, // + (byte) 0x50, (byte) 0x60, (byte) 0x70, (byte) 0x80, // + (byte) 0x90, (byte) 0xa0}; + + final byte[] nonce = {(byte) 0x09, (byte) 0xf9, (byte) 0x11, (byte) 0x02, // + (byte) 0x9d, (byte) 0x74, (byte) 0xe3, (byte) 0x5b, // + (byte) 0xd8, (byte) 0x41, (byte) 0x56, (byte) 0xc5, // + (byte) 0x63, (byte) 0x56, (byte) 0x88, (byte) 0xc0}; + + final byte[] plaintext = {(byte) 0x74, (byte) 0x68, (byte) 0x69, (byte) 0x73, // + (byte) 0x20, (byte) 0x69, (byte) 0x73, (byte) 0x20, // + (byte) 0x73, (byte) 0x6f, (byte) 0x6d, (byte) 0x65, // + (byte) 0x20, (byte) 0x70, (byte) 0x6c, (byte) 0x61, // + (byte) 0x69, (byte) 0x6e, (byte) 0x74, (byte) 0x65, // + (byte) 0x78, (byte) 0x74, (byte) 0x20, (byte) 0x74, // + (byte) 0x6f, (byte) 0x20, (byte) 0x65, (byte) 0x6e, // + (byte) 0x63, (byte) 0x72, (byte) 0x79, (byte) 0x70, // + (byte) 0x74, (byte) 0x20, (byte) 0x75, (byte) 0x73, // + (byte) 0x69, (byte) 0x6e, (byte) 0x67, (byte) 0x20, // + (byte) 0x53, (byte) 0x49, (byte) 0x56, (byte) 0x2d, // + (byte) 0x41, (byte) 0x45, (byte) 0x53}; + + 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, // + (byte) 0xcb, (byte) 0x90, (byte) 0x0f, (byte) 0x2f, // + (byte) 0xdd, (byte) 0xbe, (byte) 0x40, (byte) 0x43, // + (byte) 0x26, (byte) 0x60, (byte) 0x19, (byte) 0x65, // + (byte) 0xc8, (byte) 0x89, (byte) 0xbf, (byte) 0x17, // + (byte) 0xdb, (byte) 0xa7, (byte) 0x7c, (byte) 0xeb, // + (byte) 0x09, (byte) 0x4f, (byte) 0xa6, (byte) 0x63, // + (byte) 0xb7, (byte) 0xa3, (byte) 0xf7, (byte) 0x48, // + (byte) 0xba, (byte) 0x8a, (byte) 0xf8, (byte) 0x29, // + (byte) 0xea, (byte) 0x64, (byte) 0xad, (byte) 0x54, // + (byte) 0x4a, (byte) 0x27, (byte) 0x2e, (byte) 0x9c, // + (byte) 0x48, (byte) 0x5b, (byte) 0x62, (byte) 0xa3, // + (byte) 0xfd, (byte) 0x5c, (byte) 0x0d}; + + @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 SivEngine(key).computeCtr(new byte[48], ctr); + Assertions.assertArrayEquals(expected, result); + } + + @Test + public void testSivEncrypt() { + final byte[] result = new SivEngine(key).encrypt(plaintext, ad1, ad2, nonce); + Assertions.assertArrayEquals(ciphertext, result); + } + + @Test + public void testSivDecrypt() throws AEADBadTagException, IllegalBlockSizeException { + final byte[] result = new SivEngine(key).decrypt(ciphertext, ad1, ad2, nonce); + Assertions.assertArrayEquals(plaintext, result); + } + + } + + @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()); + } + } + + @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()); + 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); + }); + } + + } + +} diff --git a/src/test/java/org/cryptomator/siv/SivModeBenchmark.java b/src/test/java/org/cryptomator/siv/SivModeBenchmark.java index 8d7e610..1bfc271 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; @@ -19,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; @@ -32,7 +22,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) @@ -41,60 +30,28 @@ public class SivModeBenchmark { private int run; - private final byte[] encKey = new byte[16]; - private final byte[] macKey = new byte[16]; - private final byte[] cleartextData = new byte[1000]; + private final byte[] key = new byte[32]; + @Param({"1024", "1048576", "10485760"}) + private int cleartextDataSize; + private byte[] cleartextData; 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(); - } - - }); + private SivEngine 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 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 { - byte[] encrypted = jceSivMode.encrypt(encKey, macKey, cleartextData, associatedData); - byte[] decrypted = jceSivMode.decrypt(encKey, macKey, encrypted, associatedData); - Assertions.assertArrayEquals(cleartextData, decrypted); - bh.consume(encrypted); - 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); + public void benchmarkJce(Blackhole bh) throws AEADBadTagException, IllegalBlockSizeException { + 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 7002df3..ae648c1 100644 --- a/src/test/java/org/cryptomator/siv/SivModeTest.java +++ b/src/test/java/org/cryptomator/siv/SivModeTest.java @@ -1,667 +1,36 @@ 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.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; +import org.junit.jupiter.api.DisplayName; 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; -import java.io.InputStreamReader; -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; -/** - * 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 { +@Deprecated +@DisplayName("test deprecated SivMode API") +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 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); - 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]); - }); + @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 - 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]); - }); + @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); } - @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]); - }); - } - - @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); - - 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 - @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); - } - - @Test - public void testS2v() { - 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[] 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, // - (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) 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[] 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, // - (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[] 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}; - - 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 - */ - @Test - public void testNonceBasedAuthenticatedEncryption() { - final byte[] macKey = {(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}; - - final byte[] aesKey = {(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[] ad1 = {(byte) 0x00, (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, (byte) 0xff, // - (byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, // - (byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, // - (byte) 0xff, (byte) 0xee, (byte) 0xdd, (byte) 0xcc, // - (byte) 0xbb, (byte) 0xaa, (byte) 0x99, (byte) 0x88, // - (byte) 0x77, (byte) 0x66, (byte) 0x55, (byte) 0x44, // - (byte) 0x33, (byte) 0x22, (byte) 0x11, (byte) 0x00}; - - final byte[] ad2 = {(byte) 0x10, (byte) 0x20, (byte) 0x30, (byte) 0x40, // - (byte) 0x50, (byte) 0x60, (byte) 0x70, (byte) 0x80, // - (byte) 0x90, (byte) 0xa0}; - - final byte[] nonce = {(byte) 0x09, (byte) 0xf9, (byte) 0x11, (byte) 0x02, // - (byte) 0x9d, (byte) 0x74, (byte) 0xe3, (byte) 0x5b, // - (byte) 0xd8, (byte) 0x41, (byte) 0x56, (byte) 0xc5, // - (byte) 0x63, (byte) 0x56, (byte) 0x88, (byte) 0xc0}; - - final byte[] plaintext = {(byte) 0x74, (byte) 0x68, (byte) 0x69, (byte) 0x73, // - (byte) 0x20, (byte) 0x69, (byte) 0x73, (byte) 0x20, // - (byte) 0x73, (byte) 0x6f, (byte) 0x6d, (byte) 0x65, // - (byte) 0x20, (byte) 0x70, (byte) 0x6c, (byte) 0x61, // - (byte) 0x69, (byte) 0x6e, (byte) 0x74, (byte) 0x65, // - (byte) 0x78, (byte) 0x74, (byte) 0x20, (byte) 0x74, // - (byte) 0x6f, (byte) 0x20, (byte) 0x65, (byte) 0x6e, // - (byte) 0x63, (byte) 0x72, (byte) 0x79, (byte) 0x70, // - (byte) 0x74, (byte) 0x20, (byte) 0x75, (byte) 0x73, // - (byte) 0x69, (byte) 0x6e, (byte) 0x67, (byte) 0x20, // - (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, // - (byte) 0x43, (byte) 0x26, (byte) 0x67, (byte) 0xeb, // - (byte) 0x06, (byte) 0xf4, (byte) 0xd1, (byte) 0x4b, // - (byte) 0xff, (byte) 0x2f, (byte) 0xbd, (byte) 0x0f, // - (byte) 0xcb, (byte) 0x90, (byte) 0x0f, (byte) 0x2f, // - (byte) 0xdd, (byte) 0xbe, (byte) 0x40, (byte) 0x43, // - (byte) 0x26, (byte) 0x60, (byte) 0x19, (byte) 0x65, // - (byte) 0xc8, (byte) 0x89, (byte) 0xbf, (byte) 0x17, // - (byte) 0xdb, (byte) 0xa7, (byte) 0x7c, (byte) 0xeb, // - (byte) 0x09, (byte) 0x4f, (byte) 0xa6, (byte) 0x63, // - (byte) 0xb7, (byte) 0xa3, (byte) 0xf7, (byte) 0x48, // - (byte) 0xba, (byte) 0x8a, (byte) 0xf8, (byte) 0x29, // - (byte) 0xea, (byte) 0x64, (byte) 0xad, (byte) 0x54, // - (byte) 0x4a, (byte) 0x27, (byte) 0x2e, (byte) 0x9c, // - (byte) 0x48, (byte) 0x5b, (byte) 0x62, (byte) 0xa3, // - (byte) 0xfd, (byte) 0x5c, (byte) 0x0d}; - - Assertions.assertArrayEquals(expected, 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); - - 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 - 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); - } - }); - SivMode sivMode = new SivMode(); - return lines.map(EncryptionTestCase::fromLine).map(testCase -> { - int testIdx = testCase.getTestCaseNumber(); - return DynamicContainer.dynamicContainer("test case " + testIdx, Arrays.asList( - DynamicTest.dynamicTest("decrypt", () -> { - byte[] actualPlaintext = sivMode.decrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getCiphertext(), testCase.getAssociatedData()); - Assertions.assertArrayEquals(testCase.getPlaintext(), actualPlaintext); - }), - DynamicTest.dynamicTest("encrypt", () -> { - byte[] actualCiphertext = sivMode.encrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getPlaintext(), testCase.getAssociatedData()); - Assertions.assertArrayEquals(testCase.getCiphertext(), actualCiphertext); - }), - DynamicTest.dynamicTest("decrypt fails due to tampered MAC", () -> { - byte[] macKey = testCase.getMacKey(); - - // Pick some arbitrary key byte to tamper with - int tamperedByteIndex = testIdx % macKey.length; - - // Flip a single bit - macKey[tamperedByteIndex] ^= 0x10; - - Assertions.assertThrows(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCtrKey(), macKey, 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(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCtrKey(), testCase.getMacKey(), 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(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCtrKey(), testCase.getMacKey(), 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(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCtrKey(), testCase.getMacKey(), 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(UnauthenticCiphertextException.class, () -> { - sivMode.decrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getCiphertext(), appendedAd); - }); - }) - )); - }); - } - - private Provider getSunJceProvider() { - Provider provider = Security.getProvider("SunJCE"); - Assertions.assertNotNull(provider); - return provider; - } -} +} \ No newline at end of file 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..f417974 --- /dev/null +++ b/src/test/java/org/cryptomator/siv/SivProviderTest.java @@ -0,0 +1,52 @@ +package org.cryptomator.siv; + +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 { + + @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")); + } + + @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 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..621c7c7 --- /dev/null +++ b/src/test/java/org/cryptomator/siv/UtilsTest.java @@ -0,0 +1,65 @@ +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; + +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})); + } + +} \ No newline at end of file