This is an automated email from the ASF dual-hosted git repository. svenmeier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/wicket.git
commit 4c9d89d6af39250bb92069fed28aa3eedbd7276d Author: Sven Meier <[email protected]> AuthorDate: Tue Oct 30 09:13:41 2018 +0100 WICKET-6559 new encrypting page store --- .../apache/wicket/DefaultPageManagerProvider.java | 21 +++ .../apache/wicket/pageStore/CryptingPageStore.java | 151 +++++++++++++++++++++ .../wicket/pageStore/crypt/DefaultCrypter.java | 108 +++++++++++++++ .../apache/wicket/pageStore/crypt/ICrypter.java | 28 ++++ .../org/apache/wicket/settings/StoreSettings.java | 26 +++- .../wicket/pageStore/CryptingPageStoreTest.java | 88 ++++++++++++ .../devutils/pagestore/browser/PersistedPanel.java | 3 +- .../pagestore/browser/SessionIdentifiersModel.java | 1 - 8 files changed, 422 insertions(+), 4 deletions(-) diff --git a/wicket-core/src/main/java/org/apache/wicket/DefaultPageManagerProvider.java b/wicket-core/src/main/java/org/apache/wicket/DefaultPageManagerProvider.java index 8825ec6..1e8ec6f 100644 --- a/wicket-core/src/main/java/org/apache/wicket/DefaultPageManagerProvider.java +++ b/wicket-core/src/main/java/org/apache/wicket/DefaultPageManagerProvider.java @@ -21,6 +21,7 @@ import java.io.File; import org.apache.wicket.page.IPageManager; import org.apache.wicket.page.PageManager; import org.apache.wicket.pageStore.AsynchronousPageStore; +import org.apache.wicket.pageStore.CryptingPageStore; import org.apache.wicket.pageStore.DiskPageStore; import org.apache.wicket.pageStore.FilePageStore; import org.apache.wicket.pageStore.GroupingPageStore; @@ -44,6 +45,7 @@ import org.apache.wicket.util.lang.Bytes; * <li>{@link InSessionPageStore} keeping the last accessed page in the session</li> * <li>{@link AsynchronousPageStore} moving storage of pages to an asynchronous worker thread (enabled by default with {@link StoreSettings#isAsynchronous()})</li> * <li>{@link SerializingPageStore} serializing all pages (so they are available for back-button)</li> + * <li>{@link CryptingPageStore} encrypting all pages (disabled by default in {@link StoreSettings#isEncrypted()})</li> * <li>{@link DiskPageStore} persisting all pages, configured according to {@link StoreSettings}</li> * </ol> * An alternative chain with all pages held in-memory could be: @@ -95,6 +97,8 @@ public class DefaultPageManagerProvider implements IPageManagerProvider { IPageStore store = newPersistentStore(); + store = newCryptingStore(store); + store = newSerializingStore(store); store = newAsynchronousStore(store); @@ -169,6 +173,23 @@ public class DefaultPageManagerProvider implements IPageManagerProvider } /** + * Crypt all pages, if enabled in {@link StoreSettings#isEncrypted()}. + * + * @see CryptingPageStore + */ + protected IPageStore newCryptingStore(IPageStore pageStore) + { + StoreSettings storeSettings = application.getStoreSettings(); + + if (storeSettings.isEncrypted()) + { + pageStore = new CryptingPageStore(pageStore); + } + + return pageStore; + } + + /** * Keep persistent copies of all pages on disk. * * @see DiskPageStore 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 new file mode 100644 index 0000000..74cd289 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/pageStore/CryptingPageStore.java @@ -0,0 +1,151 @@ +/* + * 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; + +import java.io.Serializable; + +import org.apache.wicket.MetaDataKey; +import org.apache.wicket.WicketRuntimeException; +import org.apache.wicket.page.IManageablePage; +import org.apache.wicket.pageStore.crypt.DefaultCrypter; +import org.apache.wicket.pageStore.crypt.ICrypter; +import org.apache.wicket.util.lang.Args; + +/** + * A store that encrypts all pages before delegating and vice versa. + * <p> + * All pages passing through this store are restricted to be {@link SerializedPage}s. You can + * achieve this with + * <ul> + * <li>a {@link SerializingPageStore} delegating to this store and</li> + * <li>delegating to a store that does not deserialize its pages, e.g. a {@link DiskPageStore}.</li>. + * </ul> + */ +public class CryptingPageStore extends DelegatingPageStore +{ + private static final MetaDataKey<SessionData> KEY = new MetaDataKey<SessionData>() + { + }; + + /** + * @param delegate + * store to delegate to + * @param applicationName + * name of application + */ + public CryptingPageStore(IPageStore delegate) + { + super(delegate); + } + + /** + * Pages are always serialized, so versioning is supported. + */ + @Override + public boolean supportsVersioning() + { + return true; + } + + /** + * Supports asynchronous add if the delegate supports it. + */ + @Override + public boolean canBeAsynchronous(IPageContext context) + { + // session data must be added here *before* any asynchronous calls + // when session is no longer available + getSessionData(context); + + return getDelegate().canBeAsynchronous(context); + } + + private SessionData getSessionData(IPageContext context) + { + return context.getSessionData(KEY, () -> new SessionData(newCrypter(context))); + } + + /** + * Create a new {@link ICrypter} for the given context. + */ + protected ICrypter newCrypter(IPageContext context) { + return new DefaultCrypter(); + } + + @Override + public IManageablePage getPage(IPageContext context, int id) + { + IManageablePage page = getDelegate().getPage(context, id); + + if (page != null) + { + if (page instanceof SerializedPage == false) + { + throw new WicketRuntimeException("CryptingPageStore expects serialized pages"); + } + SerializedPage serializedPage = (SerializedPage)page; + + byte[] encrypted = serializedPage.getData(); + byte[] decrypted = getSessionData(context).decrypt(encrypted); + + page = new SerializedPage(page.getPageId(), serializedPage.getPageType(), decrypted); + } + + return page; + } + + @Override + public void addPage(IPageContext context, IManageablePage page) + { + if (page instanceof SerializedPage == false) + { + throw new WicketRuntimeException("CryptingPageStore works with serialized pages only"); + } + + SerializedPage serializedPage = (SerializedPage)page; + + byte[] decrypted = serializedPage.getData(); + byte[] encrypted = getSessionData(context).encrypt(decrypted); + + page = new SerializedPage(page.getPageId(), serializedPage.getPageType(), encrypted); + + getDelegate().addPage(context, page); + } + + private static class SessionData implements Serializable + { + + private final ICrypter cypter; + + public SessionData(ICrypter crypter) + { + Args.notNull(crypter, "crypter"); + + this.cypter= crypter; + } + + public byte[] encrypt(byte[] decrypted) + { + return cypter.encrypt(decrypted); + } + + public byte[] decrypt(byte[] encrypted) + { + return cypter.decrypt(encrypted); + } + } +} \ No newline at end of file diff --git a/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/DefaultCrypter.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/DefaultCrypter.java new file mode 100644 index 0000000..53c63be --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/DefaultCrypter.java @@ -0,0 +1,108 @@ +/* + * 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.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; + +import org.apache.wicket.WicketRuntimeException; + +/** + * Default encryption and decryption implementation. + */ +public class DefaultCrypter implements ICrypter +{ + private final SecureRandom random; + + private final SecretKey key; + + public DefaultCrypter() + { + try + { + random = SecureRandom.getInstance("SHA1PRNG", "SUN"); + + KeyGenerator generator = KeyGenerator.getInstance("AES"); + generator.init(256, random); + key = generator.generateKey(); + } + catch (GeneralSecurityException ex) + { + throw new WicketRuntimeException(ex); + } + } + + protected Cipher getCipher() throws GeneralSecurityException + { + return Cipher.getInstance("AES/CBC/PKCS5Padding"); + } + + @Override + public byte[] encrypt(byte[] decrypted) + { + try + { + Cipher cipher = getCipher(); + cipher.init(Cipher.ENCRYPT_MODE, key, random); + + AlgorithmParameters params = cipher.getParameters(); + byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV(); + + byte[] ciphertext = cipher.doFinal(decrypted); + + byte[] encrypted = Arrays.copyOf(iv, iv.length + ciphertext.length); + System.arraycopy(ciphertext, 0, encrypted, iv.length, ciphertext.length); + + return encrypted; + } + catch (GeneralSecurityException ex) + { + throw new WicketRuntimeException(ex); + } + } + + @Override + public byte[] decrypt(byte[] encrypted) + { + try + { + byte[] iv = new byte[16]; + byte[] ciphertext = new byte[encrypted.length - 16]; + System.arraycopy(encrypted, 0, iv, 0, iv.length); + System.arraycopy(encrypted, 16, ciphertext, 0, ciphertext.length); + + Cipher cipher = getCipher(); + cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); + 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/pageStore/crypt/ICrypter.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/ICrypter.java new file mode 100644 index 0000000..1cdbeaf --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/ICrypter.java @@ -0,0 +1,28 @@ +/* + * 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.io.Serializable; + +/** + * An encrypter and decrypter of pages. + */ +public interface ICrypter extends Serializable { + byte[] encrypt(byte[] bytes); + + byte[] decrypt(byte[] bytes); +} \ No newline at end of file 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 7c32101..6576a7c 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 @@ -49,6 +49,8 @@ public class StoreSettings private boolean asynchronous = true; + private boolean encrypted = false; + /** * Construct. * @@ -179,4 +181,26 @@ public class StoreSettings { return asynchronous; } -} \ No newline at end of file + + /** + * Sets a flag whether to wrap the configured {@link org.apache.wicket.pageStore.IPageStore} with + * {@link org.apache.wicket.pageStore.CryptingPageStore}. + * + * @param encrypted + * {@code true} to encrypt, {@code false} - otherwise + * @return {@code this} object for chaining + */ + public StoreSettings setEncrypted(boolean encrypted) + { + this.encrypted = encrypted; + return this; + } + + /** + * @return {@code true} if the storing of page is encrypted + */ + public boolean isEncrypted() + { + return encrypted; + } +} 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 new file mode 100644 index 0000000..3f12483 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java @@ -0,0 +1,88 @@ +/* + * 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.GeneralSecurityException; + +import org.apache.wicket.MockPage; +import org.apache.wicket.WicketRuntimeException; +import org.apache.wicket.mock.MockPageContext; +import org.apache.wicket.mock.MockPageStore; +import org.apache.wicket.serialize.java.JavaSerializer; +import org.junit.jupiter.api.Test; + +/** + * Test for {@link CryptingPageStore}. + * + * @author svenmeier + */ +public class CryptingPageStoreTest +{ + + @Test + void test() + { + CryptingPageStore store = new CryptingPageStore(new MockPageStore()); + JavaSerializer serializer = new JavaSerializer("test"); + + IPageContext context = new MockPageContext(); + + for (int p = 0; p < 10; p++) + { + MockPage add = new MockPage(p); + SerializedPage serializedAdd = new SerializedPage(p, "foo", serializer.serialize(add)); + store.addPage(context, serializedAdd); + + SerializedPage serializedGot = (SerializedPage)store.getPage(context, p); + MockPage got = (MockPage)serializer.deserialize(serializedGot.getData()); + assertEquals(p, got.getPageId()); + } + } + + @Test + void testFail() + { + CryptingPageStore store = new CryptingPageStore(new MockPageStore()); + JavaSerializer serializer = new JavaSerializer("test"); + + MockPageContext context = new MockPageContext(); + + int p = 42; + + MockPage add = new MockPage(p); + SerializedPage serializedAdd = new SerializedPage(p, "foo", serializer.serialize(add)); + store.addPage(context, serializedAdd); + + // remove key from session + context.clearSession(); + + try + { + SerializedPage serializedGot = (SerializedPage)store.getPage(context, p); + + MockPage got = (MockPage)serializer.deserialize(serializedGot.getData()); + assertEquals(p, got.getPageId()); + } + catch (WicketRuntimeException ex) + { + assertTrue(ex.getCause() instanceof GeneralSecurityException, "unable to decrypt with new key"); + } + } +} diff --git a/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/PersistedPanel.java b/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/PersistedPanel.java index 0ae5ab4..38031df 100644 --- a/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/PersistedPanel.java +++ b/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/PersistedPanel.java @@ -21,7 +21,6 @@ import java.util.List; import java.util.Optional; import org.apache.wicket.PageReference; -import org.apache.wicket.Session; import org.apache.wicket.ajax.AbstractAjaxTimerBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; @@ -149,7 +148,7 @@ public class PersistedPanel extends GenericPanel<IPersistentPageStore> return null; } - IPageContext context = new DefaultPageContext(Session.get()); + IPageContext context = new DefaultPageContext(); return store.getSessionIdentifier(context); } diff --git a/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/SessionIdentifiersModel.java b/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/SessionIdentifiersModel.java index 5792484..7cd8b4c 100644 --- a/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/SessionIdentifiersModel.java +++ b/wicket-devutils/src/main/java/org/apache/wicket/devutils/pagestore/browser/SessionIdentifiersModel.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.apache.wicket.Session; import org.apache.wicket.model.IModel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.pageStore.DefaultPageContext;
