This is an automated email from the ASF dual-hosted git repository. papegaaij pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/wicket.git
The following commit(s) were added to refs/heads/master by this push: new 210525f0ec WICKET-7016: Add support for AES-GCM-SIV as cipher for page store encryption 210525f0ec is described below commit 210525f0ecd30794532b5ebebfbe677daf244517 Author: Emond Papegaaij <emond.papega...@topicus.nl> 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 d4dadc97f1..f07d75e49b 100644 --- a/pom.xml +++ b/pom.xml @@ -138,6 +138,7 @@ <asm.version>9.2</asm.version> <aspectj.version>1.9.7</aspectj.version> <assertj-core.version>3.21.0</assertj-core.version> + <bouncycastle.version>1.72</bouncycastle.version> <byte-buddy.version>1.12.2</byte-buddy.version> <cdi-unit.version>4.1.0</cdi-unit.version> <commons-collections.version>3.2.2</commons-collections.version> @@ -480,6 +481,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 a699761976..7a606c8bf6 100644 --- a/wicket-core/pom.xml +++ b/wicket-core/pom.xml @@ -169,6 +169,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 3ab3c7f0ed..04cc5f45c1 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 1778055a0a..a6ab0cb5aa 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; + } + }; + } }