This is an automated email from the ASF dual-hosted git repository.
papegaaij pushed a commit to branch wicket-9.x
in repository https://gitbox.apache.org/repos/asf/wicket.git
The following commit(s) were added to refs/heads/wicket-9.x by this push:
new 7cb9c91f8f WICKET-7016: Add support for AES-GCM-SIV as cipher for page
store encryption
7cb9c91f8f is described below
commit 7cb9c91f8f9fbdbabab900514b4306889fae8aaa
Author: Emond Papegaaij <[email protected]>
AuthorDate: Fri Nov 18 11:59:31 2022 +0100
WICKET-7016: Add support for AES-GCM-SIV as cipher for page store encryption
---
pom.xml | 6 ++
wicket-core/pom.xml | 5 +
wicket-core/src/main/java/module-info.java | 1 +
.../apache/wicket/pageStore/CryptingPageStore.java | 2 +-
.../wicket/pageStore/crypt/GCMSIVCrypter.java | 107 +++++++++++++++++++++
.../org/apache/wicket/settings/StoreSettings.java | 29 ++++++
.../wicket/pageStore/CryptingPageStoreTest.java | 49 ++++++++--
7 files changed, 188 insertions(+), 11 deletions(-)
diff --git a/pom.xml b/pom.xml
index ba1e928ddf..ca2ad83798 100644
--- a/pom.xml
+++ b/pom.xml
@@ -137,6 +137,7 @@
<asm.version>9.1</asm.version>
<aspectj.version>1.9.6</aspectj.version>
<assertj-core.version>3.19.0</assertj-core.version>
+ <bouncycastle.version>1.72</bouncycastle.version>
<cdi-unit.version>4.1.0</cdi-unit.version>
<cglib.version>3.3.0</cglib.version>
<byte-buddy.version>1.11.12</byte-buddy.version>
@@ -483,6 +484,11 @@
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk18on</artifactId>
+ <version>${bouncycastle.version}</version>
+ </dependency>
<dependency>
<groupId>org.danekja</groupId>
<artifactId>jdk-serializable-functional</artifactId>
diff --git a/wicket-core/pom.xml b/wicket-core/pom.xml
index 30133d97d8..e563553fcc 100644
--- a/wicket-core/pom.xml
+++ b/wicket-core/pom.xml
@@ -168,6 +168,11 @@ org.apache.wicket.validation.validator;-noimport:=true
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-util</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk18on</artifactId>
+ <optional>true</optional>
+ </dependency>
<dependency>
<groupId>org.danekja</groupId>
<artifactId>jdk-serializable-functional</artifactId>
diff --git a/wicket-core/src/main/java/module-info.java
b/wicket-core/src/main/java/module-info.java
index de5f4bb89f..d9af7a5ead 100644
--- a/wicket-core/src/main/java/module-info.java
+++ b/wicket-core/src/main/java/module-info.java
@@ -29,6 +29,7 @@ module org.apache.wicket.core {
requires org.danekja.jdk.serializable.functional;
requires com.github.openjson;
requires org.junit.jupiter.api;
+ requires static org.bouncycastle.provider;
provides org.apache.wicket.IInitializer with org.apache.wicket.Initializer;
provides org.apache.wicket.resource.FileSystemPathService with
org.apache.wicket.resource.FileSystemJarPathService;
diff --git
a/wicket-core/src/main/java/org/apache/wicket/pageStore/CryptingPageStore.java
b/wicket-core/src/main/java/org/apache/wicket/pageStore/CryptingPageStore.java
index b8e26ac9e9..32185a620b 100644
---
a/wicket-core/src/main/java/org/apache/wicket/pageStore/CryptingPageStore.java
+++
b/wicket-core/src/main/java/org/apache/wicket/pageStore/CryptingPageStore.java
@@ -96,7 +96,7 @@ public class CryptingPageStore extends DelegatingPageStore
*/
protected ICrypter newCrypter()
{
- return new DefaultCrypter();
+ return application.getStoreSettings().getCrypter().get();
}
@Override
diff --git
a/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/GCMSIVCrypter.java
b/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/GCMSIVCrypter.java
new file mode 100644
index 0000000000..e4fff7bcc8
--- /dev/null
+++
b/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/GCMSIVCrypter.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.pageStore.crypt;
+
+import java.security.AlgorithmParameters;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+import org.apache.wicket.WicketRuntimeException;
+import org.bouncycastle.jcajce.spec.AEADParameterSpec;
+
+/**
+ * Encryption and decryption implementation using AES-256-GCM-SIV
authenticated encryption.
+ *
+ * This implementation requires Bouncy Castle. It is more secure than the
{@link DefaultCrypter},
+ * but also more expensive. Simple measurements have shown {@link
DefaultCrypter} to be about 10 to
+ * 15 times faster than this implementation. This is likely caused by
not-so-optimal implementation
+ * of the algorithm in Java by BC. When the JDK gets support for GCM-SIV
+ * (https://bugs.openjdk.org/browse/JDK-8256530), this implementation will
likely be faster than or
+ * about as fast as CBC.
+ */
+public class GCMSIVCrypter implements ICrypter
+{
+ protected Cipher getCipher() throws GeneralSecurityException
+ {
+ return Cipher.getInstance("AES/GCM-SIV/NoPadding");
+ }
+
+ @Override
+ public SecretKey generateKey(SecureRandom random)
+ {
+ try
+ {
+ KeyGenerator generator =
KeyGenerator.getInstance("AES");
+ generator.init(256, random);
+ return generator.generateKey();
+ }
+ catch (GeneralSecurityException ex)
+ {
+ throw new WicketRuntimeException(ex);
+ }
+ }
+
+ @Override
+ public byte[] encrypt(byte[] decrypted, SecretKey key, SecureRandom
random)
+ {
+ try
+ {
+ Cipher cipher = getCipher();
+ cipher.init(Cipher.ENCRYPT_MODE, key, random);
+
+ AlgorithmParameters params = cipher.getParameters();
+ byte[] nonce =
params.getParameterSpec(AEADParameterSpec.class).getNonce();
+ byte[] ciphertext = cipher.doFinal(decrypted);
+
+ byte[] encrypted = Arrays.copyOf(nonce, nonce.length +
ciphertext.length);
+ System.arraycopy(ciphertext, 0, encrypted,
nonce.length, ciphertext.length);
+
+ return encrypted;
+ }
+ catch (GeneralSecurityException ex)
+ {
+ throw new WicketRuntimeException(ex);
+ }
+ }
+
+ @Override
+ public byte[] decrypt(byte[] encrypted, SecretKey key)
+ {
+ try
+ {
+ byte[] nonce = new byte[12];
+ byte[] ciphertext = new byte[encrypted.length -
nonce.length];
+ System.arraycopy(encrypted, 0, nonce, 0, nonce.length);
+ System.arraycopy(encrypted, nonce.length, ciphertext,
0, ciphertext.length);
+
+ Cipher cipher = getCipher();
+ cipher.init(Cipher.DECRYPT_MODE, key, new
AEADParameterSpec(nonce, 128));
+ byte[] decrypted = cipher.doFinal(ciphertext);
+
+ return decrypted;
+ }
+ catch (GeneralSecurityException ex)
+ {
+ throw new WicketRuntimeException(ex);
+ }
+ }
+}
diff --git
a/wicket-core/src/main/java/org/apache/wicket/settings/StoreSettings.java
b/wicket-core/src/main/java/org/apache/wicket/settings/StoreSettings.java
index 6576a7c143..cf512e5675 100644
--- a/wicket-core/src/main/java/org/apache/wicket/settings/StoreSettings.java
+++ b/wicket-core/src/main/java/org/apache/wicket/settings/StoreSettings.java
@@ -18,9 +18,12 @@ package org.apache.wicket.settings;
import java.io.File;
import java.io.IOException;
+import java.util.function.Supplier;
import org.apache.wicket.Application;
import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.pageStore.crypt.DefaultCrypter;
+import org.apache.wicket.pageStore.crypt.ICrypter;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.lang.Bytes;
@@ -50,6 +53,8 @@ public class StoreSettings
private boolean asynchronous = true;
private boolean encrypted = false;
+
+ private Supplier<ICrypter> crypter = DefaultCrypter::new;
/**
* Construct.
@@ -203,4 +208,28 @@ public class StoreSettings
{
return encrypted;
}
+
+ /**
+ * Sets the supplier for the {@link ICrypter} used by a
+ * {@link org.apache.wicket.pageStore.CryptingPageStore}.
+ *
+ * @param crypter
+ * The new supplier for an {@link ICrypter}.
+ * @return {@code this} object for chaining
+ */
+ public StoreSettings setCrypter(Supplier<ICrypter> crypter)
+ {
+ this.crypter = crypter;
+ return this;
+ }
+
+ /**
+ * @return the supplier used to create a {@link ICrypter} for a
+ * {@link org.apache.wicket.pageStore.CryptingPageStore}. The
default is
+ * {@link DefaultCrypter}.
+ */
+ public Supplier<ICrypter> getCrypter()
+ {
+ return crypter;
+ }
}
diff --git
a/wicket-core/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java
b/wicket-core/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java
index 970f9fee1a..10f4d735f4 100644
---
a/wicket-core/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java
+++
b/wicket-core/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java
@@ -21,14 +21,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.StreamCorruptedException;
import java.security.GeneralSecurityException;
+import java.security.Security;
+import java.util.List;
import org.apache.wicket.MockPage;
import org.apache.wicket.mock.MockPageContext;
import org.apache.wicket.mock.MockPageStore;
+import org.apache.wicket.pageStore.crypt.DefaultCrypter;
+import org.apache.wicket.pageStore.crypt.GCMSIVCrypter;
+import org.apache.wicket.pageStore.crypt.ICrypter;
import org.apache.wicket.serialize.java.JavaSerializer;
import org.apache.wicket.util.tester.WicketTestCase;
-import org.apache.wicket.util.tester.WicketTester;
-import org.junit.jupiter.api.Test;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
/**
* Test for {@link CryptingPageStore}.
@@ -37,12 +44,22 @@ import org.junit.jupiter.api.Test;
*/
public class CryptingPageStoreTest extends WicketTestCase
{
+ @BeforeAll
+ public static void init()
+ {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ static List<ICrypter> crypters()
+ {
+ return List.of(new DefaultCrypter(), new GCMSIVCrypter());
+ }
- @Test
- void test()
+ @ParameterizedTest
+ @MethodSource("crypters")
+ void test(ICrypter crypter)
{
- CryptingPageStore store =
- new CryptingPageStore(new MockPageStore(),
tester.getApplication());
+ CryptingPageStore store = buildPageStore(crypter);
JavaSerializer serializer = new JavaSerializer("test");
IPageContext context = new MockPageContext();
@@ -59,11 +76,11 @@ public class CryptingPageStoreTest extends WicketTestCase
}
}
- @Test
- void testFail()
+ @ParameterizedTest
+ @MethodSource("crypters")
+ void testFail(ICrypter crypter)
{
- CryptingPageStore store =
- new CryptingPageStore(new MockPageStore(),
tester.getApplication());
+ CryptingPageStore store = buildPageStore(crypter);
JavaSerializer serializer = new JavaSerializer("test");
MockPageContext context = new MockPageContext();
@@ -92,4 +109,16 @@ public class CryptingPageStoreTest extends WicketTestCase
"unable to decrypt with new key");
}
}
+
+ private CryptingPageStore buildPageStore(ICrypter crypter)
+ {
+ return new CryptingPageStore(new MockPageStore(),
tester.getApplication())
+ {
+ @Override
+ protected ICrypter newCrypter()
+ {
+ return crypter;
+ }
+ };
+ }
}