Skip to content

Commit 9393f4e

Browse files
Huge performance optimizations by reusing cipher instances within the same thread.
1 parent f07ed6e commit 9393f4e

File tree

7 files changed

+81
-58
lines changed

7 files changed

+81
-58
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ public void encrypt() {
2424
byte[] decrypted = AES_SIV.decrypt(ctrKey, macKey, encrypted);
2525
}
2626

27-
public void encryptWithAdditionalData() {
28-
byte[] encrypted = AES_SIV.encrypt(ctrKey, macKey, "hello world".getBytes(), "additional".getBytes(), "data".getBytes());
29-
byte[] decrypted = AES_SIV.decrypt(ctrKey, macKey, encrypted, "additional".getBytes(), "data".getBytes());
27+
public void encryptWithAssociatedData() {
28+
byte[] encrypted = AES_SIV.encrypt(ctrKey, macKey, "hello world".getBytes(), "associated".getBytes(), "data".getBytes());
29+
byte[] decrypted = AES_SIV.decrypt(ctrKey, macKey, encrypted, "associated".getBytes(), "data".getBytes());
3030
}
3131
```
3232

@@ -37,7 +37,7 @@ public void encryptWithAdditionalData() {
3737
<dependency>
3838
<groupId>org.cryptomator</groupId>
3939
<artifactId>siv-mode</artifactId>
40-
<version>1.1.0</version>
40+
<version>1.1.1</version>
4141
</dependency>
4242
</dependencies>
4343
```

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>org.cryptomator</groupId>
55
<artifactId>siv-mode</artifactId>
6-
<version>1.1.0</version>
6+
<version>1.1.1</version>
77

88
<name>SIV Mode</name>
99
<description>RFC 5297 SIV mode: deterministic authenticated encryption</description>

src/main/java/org/cryptomator/siv/SivMode.java

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public final class SivMode {
3131
private static final byte[] BYTES_ZERO = new byte[16];
3232
private static final byte DOUBLING_CONST = (byte) 0x87;
3333

34-
private final BlockCipherFactory cipherFactory;
34+
private final ThreadLocal<BlockCipher> threadLocalCipher;
3535

3636
/**
3737
* Creates an AES-SIV instance using JCE's cipher implementation, which should normally be the best choice.<br>
@@ -52,18 +52,25 @@ public BlockCipher create() {
5252
}
5353

5454
/**
55-
* Creates an instance using a specific BlockCipher. If you want to use AES, just use the default constructor.
55+
* Creates an instance using a specific Blockcipher.get(). If you want to use AES, just use the default constructor.
5656
*
57-
* @param cipherFactory A factory method creating a BlockCipher. Must use a block size of 128 bits (16 bytes).
57+
* @param cipherFactory A factory method creating a Blockcipher.get(). Must use a block size of 128 bits (16 bytes).
5858
*/
59-
public SivMode(BlockCipherFactory cipherFactory) {
59+
public SivMode(final BlockCipherFactory cipherFactory) {
6060
// Try using cipherFactory to check that the block size is valid.
6161
// We assume here that the block size will not vary across calls to .create().
6262
if (cipherFactory.create().getBlockSize() != 16) {
6363
throw new IllegalArgumentException("cipherFactory must create BlockCipher objects with a 16-byte block size");
6464
}
6565

66-
this.cipherFactory = cipherFactory;
66+
this.threadLocalCipher = new ThreadLocal<BlockCipher>() {
67+
68+
@Override
69+
protected BlockCipher initialValue() {
70+
return cipherFactory.create();
71+
}
72+
73+
};
6774
}
6875

6976
/**
@@ -79,18 +86,18 @@ public static interface BlockCipherFactory {
7986
* @param ctrKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2
8087
* @param macKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2
8188
* @param plaintext Your plaintext, which shall be encrypted.
82-
* @param additionalData Optional additional data, which gets authenticated but not encrypted.
89+
* @param associatedData Optional associated data, which gets authenticated but not encrypted.
8390
* @return IV + Ciphertext as a concatenated byte array.
8491
* @throws IllegalArgumentException if keys are invalid or {@link SecretKey#getEncoded()} is not supported.
8592
*/
86-
public byte[] encrypt(SecretKey ctrKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) {
93+
public byte[] encrypt(SecretKey ctrKey, SecretKey macKey, byte[] plaintext, byte[]... associatedData) {
8794
final byte[] ctrKeyBytes = ctrKey.getEncoded();
8895
final byte[] macKeyBytes = macKey.getEncoded();
8996
if (ctrKeyBytes == null || macKeyBytes == null) {
9097
throw new IllegalArgumentException("Can't get bytes of given key.");
9198
}
9299
try {
93-
return encrypt(ctrKeyBytes, macKeyBytes, plaintext, additionalData);
100+
return encrypt(ctrKeyBytes, macKeyBytes, plaintext, associatedData);
94101
} finally {
95102
Arrays.fill(ctrKeyBytes, (byte) 0);
96103
Arrays.fill(macKeyBytes, (byte) 0);
@@ -103,12 +110,12 @@ public byte[] encrypt(SecretKey ctrKey, SecretKey macKey, byte[] plaintext, byte
103110
* @param ctrKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2
104111
* @param macKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2
105112
* @param plaintext Your plaintext, which shall be encrypted.
106-
* @param additionalData Optional additional data, which gets authenticated but not encrypted.
113+
* @param associatedData Optional associated data, which gets authenticated but not encrypted.
107114
* @return IV + Ciphertext as a concatenated byte array.
108115
* @throws IllegalArgumentException if the either of the two keys is of invalid length for the used {@link BlockCipher}.
109116
*/
110-
public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... additionalData) {
111-
final byte[] iv = s2v(macKey, plaintext, additionalData);
117+
public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... associatedData) {
118+
final byte[] iv = s2v(macKey, plaintext, associatedData);
112119

113120
// Check if plaintext length will cause overflows
114121
if (plaintext.length > (Integer.MAX_VALUE - 16)) {
@@ -125,7 +132,7 @@ public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]...
125132
final long initialCtrVal = ctrBuf.getLong(8);
126133

127134
final byte[] x = new byte[numBlocks * 16];
128-
final BlockCipher cipher = cipherFactory.create();
135+
final BlockCipher cipher = threadLocalCipher.get();
129136
cipher.init(true, new KeyParameter(ctrKey));
130137
for (int i = 0; i < numBlocks; i++) {
131138
final long ctrVal = initialCtrVal + i;
@@ -149,20 +156,20 @@ public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]...
149156
* @param ctrKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2
150157
* @param macKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2
151158
* @param ciphertext Your cipehrtext, which shall be decrypted.
152-
* @param additionalData Optional additional data, which needs to be authenticated during decryption.
159+
* @param associatedData Optional associated data, which needs to be authenticated during decryption.
153160
* @return Plaintext byte array.
154161
* @throws IllegalArgumentException If keys are invalid or {@link SecretKey#getEncoded()} is not supported.
155-
* @throws AEADBadTagException If the authentication failed, e.g. because ciphertext and/or additionalData are corrupted.
162+
* @throws AEADBadTagException If the authentication failed, e.g. because ciphertext and/or associatedData are corrupted.
156163
* @throws IllegalBlockSizeException If the provided ciphertext is of invalid length.
157164
*/
158-
public byte[] decrypt(SecretKey ctrKey, SecretKey macKey, byte[] ciphertext, byte[]... additionalData) throws AEADBadTagException, IllegalBlockSizeException {
165+
public byte[] decrypt(SecretKey ctrKey, SecretKey macKey, byte[] ciphertext, byte[]... associatedData) throws AEADBadTagException, IllegalBlockSizeException {
159166
final byte[] ctrKeyBytes = ctrKey.getEncoded();
160167
final byte[] macKeyBytes = macKey.getEncoded();
161168
if (ctrKeyBytes == null || macKeyBytes == null) {
162169
throw new IllegalArgumentException("Can't get bytes of given key.");
163170
}
164171
try {
165-
return decrypt(ctrKeyBytes, macKeyBytes, ciphertext, additionalData);
172+
return decrypt(ctrKeyBytes, macKeyBytes, ciphertext, associatedData);
166173
} finally {
167174
Arrays.fill(ctrKeyBytes, (byte) 0);
168175
Arrays.fill(macKeyBytes, (byte) 0);
@@ -175,13 +182,13 @@ public byte[] decrypt(SecretKey ctrKey, SecretKey macKey, byte[] ciphertext, byt
175182
* @param ctrKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2
176183
* @param macKey SIV mode requires two separate keys. You can use one long key, which is splitted in half. See https://tools.ietf.org/html/rfc5297#section-2.2
177184
* @param ciphertext Your ciphertext, which shall be encrypted.
178-
* @param additionalData Optional additional data, which needs to be authenticated during decryption.
185+
* @param associatedData Optional associated data, which needs to be authenticated during decryption.
179186
* @return Plaintext byte array.
180187
* @throws IllegalArgumentException If the either of the two keys is of invalid length for the used {@link BlockCipher}.
181-
* @throws AEADBadTagException If the authentication failed, e.g. because ciphertext and/or additionalData are corrupted.
188+
* @throws AEADBadTagException If the authentication failed, e.g. because ciphertext and/or associatedData are corrupted.
182189
* @throws IllegalBlockSizeException If the provided ciphertext is of invalid length.
183190
*/
184-
public byte[] decrypt(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]... additionalData) throws AEADBadTagException, IllegalBlockSizeException {
191+
public byte[] decrypt(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]... associatedData) throws AEADBadTagException, IllegalBlockSizeException {
185192
if (ciphertext.length < 16) {
186193
throw new IllegalBlockSizeException("Input length must be greater than or equal 16.");
187194
}
@@ -200,7 +207,7 @@ public byte[] decrypt(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]...
200207
final long initialCtrVal = ctrBuf.getLong(8);
201208

202209
final byte[] x = new byte[numBlocks * 16];
203-
final BlockCipher cipher = cipherFactory.create();
210+
final BlockCipher cipher = threadLocalCipher.get();
204211
cipher.init(true, new KeyParameter(ctrKey));
205212
for (int i = 0; i < numBlocks; i++) {
206213
final long ctrVal = initialCtrVal + i;
@@ -211,7 +218,7 @@ public byte[] decrypt(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]...
211218

212219
final byte[] plaintext = xor(actualCiphertext, x);
213220

214-
final byte[] control = s2v(macKey, plaintext, additionalData);
221+
final byte[] control = s2v(macKey, plaintext, associatedData);
215222

216223
// time-constant comparison (taken from MessageDigest.isEqual in JDK8)
217224
assert iv.length == control.length;
@@ -228,21 +235,20 @@ public byte[] decrypt(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]...
228235
}
229236

230237
// Visible for testing, throws IllegalArgumentException if key is not accepted by CMac#init(CipherParameters)
231-
byte[] s2v(byte[] macKey, byte[] plaintext, byte[]... additionalData) {
238+
byte[] s2v(byte[] macKey, byte[] plaintext, byte[]... associatedData) {
232239
// Maximum permitted AD length is the block size in bits - 2
233-
if (additionalData.length > 126) {
240+
if (associatedData.length > 126) {
234241
// SIV mode cannot be used safely with this many AD fields
235-
throw new IllegalArgumentException("too many Additional Data fields");
242+
throw new IllegalArgumentException("too many Associated Data fields");
236243
}
237244

238245
final CipherParameters params = new KeyParameter(macKey);
239-
final BlockCipher cipher = cipherFactory.create();
240-
final CMac mac = new CMac(cipher);
246+
final CMac mac = new CMac(threadLocalCipher.get());
241247
mac.init(params);
242248

243249
byte[] d = mac(mac, BYTES_ZERO);
244250

245-
for (byte[] s : additionalData) {
251+
for (byte[] s : associatedData) {
246252
d = xor(dbl(d), mac(mac, s));
247253
}
248254

src/test/java/org/cryptomator/siv/BenchmarkTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public void runBenchmarks() throws RunnerException {
2323
// Specify which benchmarks to run
2424
.include(getClass().getPackage().getName() + ".*Benchmark.*")
2525
// Set the following options as needed
26-
.threads(2).forks(1) //
26+
.threads(2).forks(2) //
2727
.shouldFailOnError(true).shouldDoGC(true)
2828
// .jvmArgs("-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintInlining")
2929
// .addProfiler(WinPerfAsmProfiler.class)

src/test/java/org/cryptomator/siv/EncryptionTestCase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public byte[] getPlaintext() {
7575
return Arrays.copyOf(plaintext, plaintext.length);
7676
}
7777

78-
public byte[][] getAdditionalData() {
78+
public byte[][] getAssociatedData() {
7979
final byte[][] result = new byte[additionalData.length][];
8080

8181
for (int i = 0; i < additionalData.length; i++) {

src/test/java/org/cryptomator/siv/SivModeBenchmark.java

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@
1111
import java.util.Arrays;
1212
import java.util.concurrent.TimeUnit;
1313

14+
import javax.crypto.AEADBadTagException;
15+
import javax.crypto.IllegalBlockSizeException;
16+
1417
import org.bouncycastle.crypto.BlockCipher;
1518
import org.bouncycastle.crypto.engines.AESFastEngine;
1619
import org.bouncycastle.crypto.engines.AESLightEngine;
1720
import org.cryptomator.siv.SivMode.BlockCipherFactory;
21+
import org.junit.Assert;
1822
import org.openjdk.jmh.annotations.Benchmark;
1923
import org.openjdk.jmh.annotations.BenchmarkMode;
2024
import org.openjdk.jmh.annotations.Level;
@@ -25,22 +29,23 @@
2529
import org.openjdk.jmh.annotations.Setup;
2630
import org.openjdk.jmh.annotations.State;
2731
import org.openjdk.jmh.annotations.Warmup;
32+
import org.openjdk.jmh.infra.Blackhole;
2833

2934
/**
3035
* Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
3136
*/
3237
@State(Scope.Thread)
33-
@Warmup(iterations = 2, time = 500, timeUnit = TimeUnit.MILLISECONDS)
38+
@Warmup(iterations = 3, time = 300, timeUnit = TimeUnit.MILLISECONDS)
3439
@Measurement(iterations = 2, time = 500, timeUnit = TimeUnit.MILLISECONDS)
3540
@BenchmarkMode(value = {Mode.AverageTime})
3641
@OutputTimeUnit(TimeUnit.MICROSECONDS)
3742
public class SivModeBenchmark {
3843

3944
private int run;
40-
private final byte[] encKeyBuf = new byte[16];
41-
private final byte[] macKeyBuf = new byte[16];
42-
private final byte[] testData = new byte[8 * 1024];
43-
private final byte[] adData = new byte[1024];
45+
private final byte[] encKey = new byte[16];
46+
private final byte[] macKey = new byte[16];
47+
private final byte[] cleartextData = new byte[1000];
48+
private final byte[] associatedData = new byte[100];
4449

4550
private final SivMode jceSivMode = new SivMode();
4651
private final SivMode bcFastSivMode = new SivMode(new BlockCipherFactory() {
@@ -63,25 +68,37 @@ public BlockCipher create() {
6368
@Setup(Level.Trial)
6469
public void shuffleData() {
6570
run++;
66-
Arrays.fill(encKeyBuf, (byte) (run & 0xFF));
67-
Arrays.fill(macKeyBuf, (byte) (run & 0xFF));
68-
Arrays.fill(testData, (byte) (run & 0xFF));
69-
Arrays.fill(adData, (byte) (run & 0xFF));
71+
Arrays.fill(encKey, (byte) (run & 0xFF));
72+
Arrays.fill(macKey, (byte) (run & 0xFF));
73+
Arrays.fill(cleartextData, (byte) (run & 0xFF));
74+
Arrays.fill(associatedData, (byte) (run & 0xFF));
7075
}
7176

7277
@Benchmark
73-
public void benchmarkJce() {
74-
jceSivMode.encrypt(encKeyBuf, macKeyBuf, testData, adData);
78+
public void benchmarkJce(Blackhole bh) throws AEADBadTagException, IllegalBlockSizeException {
79+
byte[] encrypted = jceSivMode.encrypt(encKey, macKey, cleartextData, associatedData);
80+
byte[] decrypted = jceSivMode.decrypt(encKey, macKey, encrypted, associatedData);
81+
Assert.assertArrayEquals(cleartextData, decrypted);
82+
bh.consume(encrypted);
83+
bh.consume(decrypted);
7584
}
7685

7786
@Benchmark
78-
public void benchmarkBcFast() {
79-
bcFastSivMode.encrypt(encKeyBuf, macKeyBuf, testData, adData);
87+
public void benchmarkBcFast(Blackhole bh) throws AEADBadTagException, IllegalBlockSizeException {
88+
byte[] encrypted = bcFastSivMode.encrypt(encKey, macKey, cleartextData, associatedData);
89+
byte[] decrypted = bcFastSivMode.decrypt(encKey, macKey, encrypted, associatedData);
90+
Assert.assertArrayEquals(cleartextData, decrypted);
91+
bh.consume(encrypted);
92+
bh.consume(decrypted);
8093
}
8194

8295
@Benchmark
83-
public void benchmarkBcLight() {
84-
bcLightSivMode.encrypt(encKeyBuf, macKeyBuf, testData, adData);
96+
public void benchmarkBcLight(Blackhole bh) throws AEADBadTagException, IllegalBlockSizeException {
97+
byte[] encrypted = bcLightSivMode.encrypt(encKey, macKey, cleartextData, associatedData);
98+
byte[] decrypted = bcLightSivMode.decrypt(encKey, macKey, encrypted, associatedData);
99+
Assert.assertArrayEquals(cleartextData, decrypted);
100+
bh.consume(encrypted);
101+
bh.consume(decrypted);
85102
}
86103

87104
}

src/test/java/org/cryptomator/siv/SivModeTest.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public void testDecryptWithInvalidBlockSize() throws AEADBadTagException, Illega
105105
}
106106

107107
@Test(expected = IllegalArgumentException.class)
108-
public void testEncryptAdditionalDataLimit() {
108+
public void testEncryptAssociatedDataLimit() {
109109
final byte[] ctrKey = new byte[16];
110110
final byte[] macKey = new byte[16];
111111
final byte[] plaintext = new byte[30];
@@ -114,7 +114,7 @@ public void testEncryptAdditionalDataLimit() {
114114
}
115115

116116
@Test(expected = IllegalArgumentException.class)
117-
public void testDecryptAdditionalDataLimit() throws AEADBadTagException, IllegalBlockSizeException {
117+
public void testDecryptAssociatedDataLimit() throws AEADBadTagException, IllegalBlockSizeException {
118118
final byte[] ctrKey = new byte[16];
119119
final byte[] macKey = new byte[16];
120120
final byte[] plaintext = new byte[80];
@@ -438,7 +438,7 @@ public void testGeneratedTestCases() throws IOException, AEADBadTagException, Il
438438
macKey[tamperedByteIndex] ^= 0x10;
439439

440440
try {
441-
new SivMode().decrypt(testCase.getCtrKey(), macKey, testCase.getCiphertext(), testCase.getAdditionalData());
441+
new SivMode().decrypt(testCase.getCtrKey(), macKey, testCase.getCiphertext(), testCase.getAssociatedData());
442442
Assert.fail();
443443
} catch (AEADBadTagException ex) {
444444
// Test case passed.
@@ -458,19 +458,19 @@ public void testGeneratedTestCases() throws IOException, AEADBadTagException, Il
458458
ciphertext[tamperedByteIndex] ^= 0x10;
459459

460460
try {
461-
new SivMode().decrypt(testCase.getCtrKey(), testCase.getMacKey(), ciphertext, testCase.getAdditionalData());
461+
new SivMode().decrypt(testCase.getCtrKey(), testCase.getMacKey(), ciphertext, testCase.getAssociatedData());
462462
Assert.fail();
463463
} catch (AEADBadTagException ex) {
464464
// Test case passed.
465465
}
466466
}
467467

468-
// Check that decryption fails if additional data is tampered with
468+
// Check that decryption fails if associated data is tampered with
469469
for (int testCaseIdx = 0; testCaseIdx < allTestCases.length; testCaseIdx++) {
470470
EncryptionTestCase testCase = allTestCases[testCaseIdx];
471-
byte[][] ad = testCase.getAdditionalData();
471+
byte[][] ad = testCase.getAssociatedData();
472472

473-
// Try flipping bits in the additional data elements
473+
// Try flipping bits in the associated data elements
474474
for (int adIdx = 0; adIdx < ad.length; adIdx++) {
475475
// Skip if this ad element is empty
476476
if (ad[adIdx].length == 0) {
@@ -522,13 +522,13 @@ public void testGeneratedTestCases() throws IOException, AEADBadTagException, Il
522522

523523
// Check that ciphertexts/IVs are produced correctly
524524
for (EncryptionTestCase testCase : allTestCases) {
525-
final byte[] actualCiphertext = new SivMode().encrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getPlaintext(), testCase.getAdditionalData());
525+
final byte[] actualCiphertext = new SivMode().encrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getPlaintext(), testCase.getAssociatedData());
526526
Assert.assertArrayEquals(testCase.getCiphertext(), actualCiphertext);
527527
}
528528

529529
// Check that ciphertexts are decrypted correctly
530530
for (EncryptionTestCase testCase : allTestCases) {
531-
final byte[] actualPlaintext = new SivMode().decrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getCiphertext(), testCase.getAdditionalData());
531+
final byte[] actualPlaintext = new SivMode().decrypt(testCase.getCtrKey(), testCase.getMacKey(), testCase.getCiphertext(), testCase.getAssociatedData());
532532
Assert.assertArrayEquals(testCase.getPlaintext(), actualPlaintext);
533533
}
534534
}

0 commit comments

Comments
 (0)