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;

Reply via email to