This is an automated email from the ASF dual-hosted git repository.

papegaaij pushed a commit to branch crypt-unification
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 6b85d7c81a1db49e976f74dd5cd4c6b5b919c561
Author: Emond Papegaaij <[email protected]>
AuthorDate: Fri Jul 3 15:42:41 2026 +0200

    WICKET-7190 Let the crypt scheme generate its own key
    
    A factory cannot know what key material an encryption scheme needs, so key
    generation is moved from the factories onto the scheme:
    
    - ICryptScheme gains an abstract generateKey(SecureRandom). The scheme owns 
the
      key material; a factory only decides where the key lives (per session, 
global,
      externally supplied). All schemes sharing one SchemeCrypt must produce
      compatible keys, since existing ciphertext is decrypted with the current 
key
      during migration.
    - New AbstractAesGcmCryptScheme carries everything the AES-256 GCM-family 
schemes
      share: the 256-bit AES key generation and the encrypt/decrypt flow 
(12-byte
      nonce, 128-bit tag, nonce||ciphertext||tag layout, marker authenticated as
      associated data). AesGcmCryptScheme and AesGcmSivCryptScheme now only 
supply
      id(), the Cipher and the AlgorithmParameterSpec, removing the duplication 
that
      existed between them.
    
    All four call sites that previously hardcoded
    CipherUtils.generateKey("AES", 256, ...) now delegate to the configured 
scheme:
    
    - AbstractCryptFactory gains a protected generateKey(random) helper 
resolving the
      scheme from SecuritySettings; KeyInSessionCryptFactory uses it.
    - ApplicationKeyCryptFactory(SecureRandom) generates its key lazily on 
first use
      (so the scheme is configured by then) and caches it; the (SecretKey)
      external-key constructor is unchanged.
    - SecuritySettings.getAuthenticationStrategy() and CryptingPageStore both 
ask the
      scheme for the key.
    
    Tests: SchemeCryptTest verifies generateKey yields a usable 256-bit AES key 
that
    round-trips; CryptFactoryTest covers the lazy application-random-key path.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../wicket/core/util/crypt/CryptFactoryTest.java   | 15 +++++
 .../wicket/core/util/crypt/SchemeCryptTest.java    | 14 ++++
 ...tScheme.java => AbstractAesGcmCryptScheme.java} | 73 ++++++++++++---------
 .../core/util/crypt/AbstractCryptFactory.java      | 15 +++++
 .../wicket/core/util/crypt/AesGcmCryptScheme.java  | 74 ++--------------------
 .../core/util/crypt/AesGcmSivCryptScheme.java      | 73 ++-------------------
 .../util/crypt/ApplicationKeyCryptFactory.java     | 25 +++++---
 .../wicket/core/util/crypt/ICryptScheme.java       | 16 ++++-
 .../core/util/crypt/KeyInSessionCryptFactory.java  | 17 -----
 .../apache/wicket/pageStore/CryptingPageStore.java |  8 +--
 .../apache/wicket/settings/SecuritySettings.java   |  5 +-
 11 files changed, 133 insertions(+), 202 deletions(-)

diff --git 
a/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/CryptFactoryTest.java
 
b/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/CryptFactoryTest.java
index 0bea8d4ccc..fe60ed8a6e 100644
--- 
a/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/CryptFactoryTest.java
+++ 
b/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/CryptFactoryTest.java
@@ -44,6 +44,21 @@ class CryptFactoryTest extends WicketTestCase
                assertArrayEquals("hello".getBytes(UTF_8), 
factory.newCrypt().decrypt(encrypted));
        }
 
+       /**
+        * The {@link 
ApplicationKeyCryptFactory#ApplicationKeyCryptFactory(java.security.SecureRandom)
+        * random-key} constructor generates its key lazily (via the configured 
scheme) and then keeps
+        * it stable across {@link ICrypt} instances.
+        */
+       @Test
+       void applicationRandomKeyIsStableAcrossCryptInstances()
+       {
+               ApplicationKeyCryptFactory factory = new 
ApplicationKeyCryptFactory(
+                       
tester.getApplication().getSecuritySettings().getRandomSupplier().getRandom());
+
+               byte[] encrypted = 
factory.newCrypt().encrypt("hello".getBytes(UTF_8));
+               assertArrayEquals("hello".getBytes(UTF_8), 
factory.newCrypt().decrypt(encrypted));
+       }
+
        /**
         * The per-session key is stable within a session, so data encrypted 
through one {@link ICrypt}
         * can be decrypted through another obtained from the same factory in 
the same session.
diff --git 
a/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/SchemeCryptTest.java
 
b/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/SchemeCryptTest.java
index a8b77552e3..c8c6136711 100644
--- 
a/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/SchemeCryptTest.java
+++ 
b/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/SchemeCryptTest.java
@@ -77,6 +77,20 @@ public class SchemeCryptTest
                assertArrayEquals(plain, crypt.decrypt(crypt.encrypt(plain)));
        }
 
+       @ParameterizedTest
+       @MethodSource("schemes")
+       void generatedKeyIsUsableAes256(ICryptScheme scheme)
+       {
+               SecretKey key = scheme.generateKey(RANDOM);
+               assertEquals("AES", key.getAlgorithm());
+               assertEquals(32, key.getEncoded().length);
+
+               // a crypt built on the scheme-generated key round-trips
+               SchemeCrypt crypt = crypt(key, scheme);
+               byte[] plain = "generated-key".getBytes(StandardCharsets.UTF_8);
+               assertArrayEquals(plain, crypt.decrypt(crypt.encrypt(plain)));
+       }
+
        @ParameterizedTest
        @MethodSource("schemes")
        void ciphertextStartsWithSchemeMarker(ICryptScheme scheme)
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmCryptScheme.java
 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractAesGcmCryptScheme.java
similarity index 54%
copy from 
wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmCryptScheme.java
copy to 
wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractAesGcmCryptScheme.java
index 1ce04fb914..88545ddef9 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmCryptScheme.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractAesGcmCryptScheme.java
@@ -18,49 +18,47 @@ package org.apache.wicket.core.util.crypt;
 
 import java.security.GeneralSecurityException;
 import java.security.SecureRandom;
+import java.security.spec.AlgorithmParameterSpec;
 import java.util.Arrays;
 
 import javax.crypto.Cipher;
 import javax.crypto.SecretKey;
-import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
 
 import org.apache.wicket.WicketRuntimeException;
+import org.apache.wicket.util.crypt.CipherUtils;
 
 /**
- * Authenticated encryption using JDK-native AES-256-GCM ({@code 
AES/GCM/NoPadding}).
- * <p>
- * This is the default scheme: it needs no extra dependencies and provides 
confidentiality and
- * integrity. The ciphertext layout is {@code nonce(12) || ciphertext || 
tag(16)}; the scheme
- * marker supplied by {@link SchemeCrypt} is authenticated as associated data.
- * <p>
- * A fresh random 96-bit nonce is generated per message. Because each key is 
used by a single
- * session (or a single application), the number of encryptions under one key 
stays far below the
- * NIST SP&nbsp;800-38D safe-usage bound for random nonces. Deployments that 
need nonce-misuse
- * resistance beyond that bound can switch to {@link AesGcmSivCryptScheme}.
+ * Base class for the AES-256 GCM-family {@link ICryptScheme}s ({@link 
AesGcmCryptScheme} and
+ * {@link AesGcmSivCryptScheme}). It owns everything the two variants share:
+ * <ul>
+ * <li>the 256-bit AES key they consume (see {@link 
#generateKey(SecureRandom)}) &mdash; the key is
+ * a property of the scheme, not of the factory that decides where the key 
lives;</li>
+ * <li>the ciphertext layout {@code nonce(12) || ciphertext || tag(16)} and 
the encrypt/decrypt
+ * flow (random 96-bit nonce, 128-bit authentication tag, marker authenticated 
as associated
+ * data).</li>
+ * </ul>
+ * Subclasses only supply the concrete {@link Cipher} and its {@link 
AlgorithmParameterSpec}, plus a
+ * stable {@link #id()}.
  */
-public class AesGcmCryptScheme implements ICryptScheme
+public abstract class AbstractAesGcmCryptScheme implements ICryptScheme
 {
-       /** Stable marker id for this scheme. */
-       public static final byte ID = 1;
+       /** AES key size in bits. */
+       protected static final int KEY_LENGTH_BITS = 256;
 
-       private static final int NONCE_LENGTH = 12;
+       /** Nonce (IV) length in bytes; 96 bits is the GCM-family recommended 
size. */
+       protected static final int NONCE_LENGTH = 12;
 
-       private static final int TAG_LENGTH_BITS = 128;
-
-       /**
-        * @return the {@link Cipher} to use
-        * @throws GeneralSecurityException
-        *             if the cipher is unavailable
-        */
-       protected Cipher getCipher() throws GeneralSecurityException
-       {
-               return Cipher.getInstance("AES/GCM/NoPadding");
-       }
+       /** Authentication tag length in bits. */
+       protected static final int TAG_LENGTH_BITS = 128;
 
        @Override
-       public byte id()
+       public SecretKey generateKey(SecureRandom random)
        {
-               return ID;
+               // wrap in a SecretKeySpec so the key is guaranteed 
serializable (a per-session key is
+               // stored in the session's metadata)
+               byte[] encoded = CipherUtils.generateKey("AES", 
KEY_LENGTH_BITS, random).getEncoded();
+               return new SecretKeySpec(encoded, "AES");
        }
 
        @Override
@@ -72,8 +70,7 @@ public class AesGcmCryptScheme implements ICryptScheme
                        random.nextBytes(nonce);
 
                        Cipher cipher = getCipher();
-                       cipher.init(Cipher.ENCRYPT_MODE, key, new 
GCMParameterSpec(TAG_LENGTH_BITS, nonce),
-                               random);
+                       cipher.init(Cipher.ENCRYPT_MODE, key, 
newParameterSpec(nonce), random);
                        if (aad != null)
                        {
                                cipher.updateAAD(aad);
@@ -104,7 +101,7 @@ public class AesGcmCryptScheme implements ICryptScheme
                        byte[] nonce = Arrays.copyOfRange(ciphertext, 0, 
NONCE_LENGTH);
 
                        Cipher cipher = getCipher();
-                       cipher.init(Cipher.DECRYPT_MODE, key, new 
GCMParameterSpec(TAG_LENGTH_BITS, nonce));
+                       cipher.init(Cipher.DECRYPT_MODE, key, 
newParameterSpec(nonce));
                        if (aad != null)
                        {
                                cipher.updateAAD(aad);
@@ -118,4 +115,18 @@ public class AesGcmCryptScheme implements ICryptScheme
                        return null;
                }
        }
+
+       /**
+        * @return the {@link Cipher} to use
+        * @throws GeneralSecurityException
+        *             if the cipher is unavailable
+        */
+       protected abstract Cipher getCipher() throws GeneralSecurityException;
+
+       /**
+        * @param nonce
+        *            the freshly generated nonce
+        * @return the {@link AlgorithmParameterSpec} binding the nonce and tag 
length for this cipher
+        */
+       protected abstract AlgorithmParameterSpec newParameterSpec(byte[] 
nonce);
 }
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractCryptFactory.java
 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractCryptFactory.java
index ed12284dde..775ba2266e 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractCryptFactory.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractCryptFactory.java
@@ -51,6 +51,21 @@ public abstract class AbstractCryptFactory implements 
ICryptFactory
                        settings.getWhitelistedCryptSchemes());
        }
 
+       /**
+        * Generates a fresh key for the application's configured
+        * {@link SecuritySettings#getCryptScheme() encryption scheme}. The 
scheme &mdash; not the
+        * factory &mdash; decides what key material to produce; the factory 
only decides where the key
+        * lives.
+        *
+        * @param random
+        *            source of randomness
+        * @return a new secret key
+        */
+       protected SecretKey generateKey(SecureRandom random)
+       {
+               return 
Application.get().getSecuritySettings().getCryptScheme().generateKey(random);
+       }
+
        /**
         * @return the secret key to use; the key source (per-session, global, 
...) is the concrete
         *         factory's responsibility
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmCryptScheme.java
 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmCryptScheme.java
index 1ce04fb914..691824234e 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmCryptScheme.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmCryptScheme.java
@@ -17,15 +17,11 @@
 package org.apache.wicket.core.util.crypt;
 
 import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
-import java.util.Arrays;
+import java.security.spec.AlgorithmParameterSpec;
 
 import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
 import javax.crypto.spec.GCMParameterSpec;
 
-import org.apache.wicket.WicketRuntimeException;
-
 /**
  * Authenticated encryption using JDK-native AES-256-GCM ({@code 
AES/GCM/NoPadding}).
  * <p>
@@ -38,25 +34,11 @@ import org.apache.wicket.WicketRuntimeException;
  * NIST SP&nbsp;800-38D safe-usage bound for random nonces. Deployments that 
need nonce-misuse
  * resistance beyond that bound can switch to {@link AesGcmSivCryptScheme}.
  */
-public class AesGcmCryptScheme implements ICryptScheme
+public class AesGcmCryptScheme extends AbstractAesGcmCryptScheme
 {
        /** Stable marker id for this scheme. */
        public static final byte ID = 1;
 
-       private static final int NONCE_LENGTH = 12;
-
-       private static final int TAG_LENGTH_BITS = 128;
-
-       /**
-        * @return the {@link Cipher} to use
-        * @throws GeneralSecurityException
-        *             if the cipher is unavailable
-        */
-       protected Cipher getCipher() throws GeneralSecurityException
-       {
-               return Cipher.getInstance("AES/GCM/NoPadding");
-       }
-
        @Override
        public byte id()
        {
@@ -64,58 +46,14 @@ public class AesGcmCryptScheme implements ICryptScheme
        }
 
        @Override
-       public byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, 
SecureRandom random)
+       protected Cipher getCipher() throws GeneralSecurityException
        {
-               try
-               {
-                       byte[] nonce = new byte[NONCE_LENGTH];
-                       random.nextBytes(nonce);
-
-                       Cipher cipher = getCipher();
-                       cipher.init(Cipher.ENCRYPT_MODE, key, new 
GCMParameterSpec(TAG_LENGTH_BITS, nonce),
-                               random);
-                       if (aad != null)
-                       {
-                               cipher.updateAAD(aad);
-                       }
-
-                       byte[] ciphertext = cipher.doFinal(plaintext);
-
-                       byte[] result = Arrays.copyOf(nonce, nonce.length + 
ciphertext.length);
-                       System.arraycopy(ciphertext, 0, result, nonce.length, 
ciphertext.length);
-                       return result;
-               }
-               catch (GeneralSecurityException ex)
-               {
-                       throw new WicketRuntimeException(ex);
-               }
+               return Cipher.getInstance("AES/GCM/NoPadding");
        }
 
        @Override
-       public byte[] decrypt(byte[] ciphertext, SecretKey key, byte[] aad)
+       protected AlgorithmParameterSpec newParameterSpec(byte[] nonce)
        {
-               try
-               {
-                       if (ciphertext.length < NONCE_LENGTH)
-                       {
-                               return null;
-                       }
-
-                       byte[] nonce = Arrays.copyOfRange(ciphertext, 0, 
NONCE_LENGTH);
-
-                       Cipher cipher = getCipher();
-                       cipher.init(Cipher.DECRYPT_MODE, key, new 
GCMParameterSpec(TAG_LENGTH_BITS, nonce));
-                       if (aad != null)
-                       {
-                               cipher.updateAAD(aad);
-                       }
-
-                       return cipher.doFinal(ciphertext, NONCE_LENGTH, 
ciphertext.length - NONCE_LENGTH);
-               }
-               catch (GeneralSecurityException ex)
-               {
-                       // authentication failure or malformed input
-                       return null;
-               }
+               return new GCMParameterSpec(TAG_LENGTH_BITS, nonce);
        }
 }
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmSivCryptScheme.java
 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmSivCryptScheme.java
index d96cd90820..54ab50a367 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmSivCryptScheme.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmSivCryptScheme.java
@@ -17,13 +17,10 @@
 package org.apache.wicket.core.util.crypt;
 
 import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
-import java.util.Arrays;
+import java.security.spec.AlgorithmParameterSpec;
 
 import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
 
-import org.apache.wicket.WicketRuntimeException;
 import org.bouncycastle.jcajce.spec.AEADParameterSpec;
 
 /**
@@ -38,25 +35,11 @@ import org.bouncycastle.jcajce.spec.AEADParameterSpec;
  * The ciphertext layout is {@code nonce(12) || ciphertext || tag(16)}; the 
scheme marker
  * supplied by {@link SchemeCrypt} is authenticated as associated data.
  */
-public class AesGcmSivCryptScheme implements ICryptScheme
+public class AesGcmSivCryptScheme extends AbstractAesGcmCryptScheme
 {
        /** Stable marker id for this scheme. */
        public static final byte ID = 2;
 
-       private static final int NONCE_LENGTH = 12;
-
-       private static final int TAG_LENGTH_BITS = 128;
-
-       /**
-        * @return the {@link Cipher} to use
-        * @throws GeneralSecurityException
-        *             if the cipher is unavailable (e.g. Bouncy Castle not 
registered)
-        */
-       protected Cipher getCipher() throws GeneralSecurityException
-       {
-               return Cipher.getInstance("AES/GCM-SIV/NoPadding");
-       }
-
        @Override
        public byte id()
        {
@@ -64,58 +47,14 @@ public class AesGcmSivCryptScheme implements ICryptScheme
        }
 
        @Override
-       public byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, 
SecureRandom random)
+       protected Cipher getCipher() throws GeneralSecurityException
        {
-               try
-               {
-                       byte[] nonce = new byte[NONCE_LENGTH];
-                       random.nextBytes(nonce);
-
-                       Cipher cipher = getCipher();
-                       cipher.init(Cipher.ENCRYPT_MODE, key, new 
AEADParameterSpec(nonce, TAG_LENGTH_BITS),
-                               random);
-                       if (aad != null)
-                       {
-                               cipher.updateAAD(aad);
-                       }
-
-                       byte[] ciphertext = cipher.doFinal(plaintext);
-
-                       byte[] result = Arrays.copyOf(nonce, nonce.length + 
ciphertext.length);
-                       System.arraycopy(ciphertext, 0, result, nonce.length, 
ciphertext.length);
-                       return result;
-               }
-               catch (GeneralSecurityException ex)
-               {
-                       throw new WicketRuntimeException(ex);
-               }
+               return Cipher.getInstance("AES/GCM-SIV/NoPadding");
        }
 
        @Override
-       public byte[] decrypt(byte[] ciphertext, SecretKey key, byte[] aad)
+       protected AlgorithmParameterSpec newParameterSpec(byte[] nonce)
        {
-               try
-               {
-                       if (ciphertext.length < NONCE_LENGTH)
-                       {
-                               return null;
-                       }
-
-                       byte[] nonce = Arrays.copyOfRange(ciphertext, 0, 
NONCE_LENGTH);
-
-                       Cipher cipher = getCipher();
-                       cipher.init(Cipher.DECRYPT_MODE, key, new 
AEADParameterSpec(nonce, TAG_LENGTH_BITS));
-                       if (aad != null)
-                       {
-                               cipher.updateAAD(aad);
-                       }
-
-                       return cipher.doFinal(ciphertext, NONCE_LENGTH, 
ciphertext.length - NONCE_LENGTH);
-               }
-               catch (GeneralSecurityException ex)
-               {
-                       // authentication failure or malformed input
-                       return null;
-               }
+               return new AEADParameterSpec(nonce, TAG_LENGTH_BITS);
        }
 }
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ApplicationKeyCryptFactory.java
 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ApplicationKeyCryptFactory.java
index 16415a458b..045de2dfce 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ApplicationKeyCryptFactory.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ApplicationKeyCryptFactory.java
@@ -19,9 +19,7 @@ package org.apache.wicket.core.util.crypt;
 import java.security.SecureRandom;
 
 import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
 
-import org.apache.wicket.util.crypt.CipherUtils;
 import org.apache.wicket.util.lang.Args;
 
 /**
@@ -31,12 +29,16 @@ import org.apache.wicket.util.lang.Args;
  * <p>
  * Use {@link #ApplicationKeyCryptFactory(SecretKey)} with a stable, 
externally-managed key to keep
  * data decryptable across application restarts. The
- * {@link #ApplicationKeyCryptFactory(SecureRandom)} constructor generates a 
random key that lives
- * only for the lifetime of this factory (so data does not survive a restart).
+ * {@link #ApplicationKeyCryptFactory(SecureRandom)} constructor instead 
generates a key (for the
+ * application's configured {@link 
org.apache.wicket.settings.SecuritySettings#getCryptScheme()
+ * scheme}) on first use; that key lives only for the lifetime of this 
factory, so data does not
+ * survive a restart.
  */
 public class ApplicationKeyCryptFactory extends AbstractCryptFactory
 {
-       private final SecretKey key;
+       private final SecureRandom random;
+
+       private SecretKey key;
 
        /**
         * @param key
@@ -45,22 +47,29 @@ public class ApplicationKeyCryptFactory extends 
AbstractCryptFactory
        public ApplicationKeyCryptFactory(SecretKey key)
        {
                this.key = Args.notNull(key, "key");
+               this.random = null;
        }
 
        /**
-        * Generates a random application-wide key. Data encrypted with it does 
not survive a restart.
+        * Generates a random application-wide key on first use. Data encrypted 
with it does not survive
+        * a restart.
         *
         * @param random
         *            source of randomness
         */
        public ApplicationKeyCryptFactory(SecureRandom random)
        {
-               this(new SecretKeySpec(CipherUtils.generateKey("AES", 256, 
random).getEncoded(), "AES"));
+               this.random = Args.notNull(random, "random");
        }
 
        @Override
-       protected SecretKey getKey()
+       protected synchronized SecretKey getKey()
        {
+               if (key == null)
+               {
+                       // generate the application-wide key lazily, once the 
scheme has been configured
+                       key = generateKey(random);
+               }
                return key;
        }
 }
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICryptScheme.java 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICryptScheme.java
index 9eeff286a2..ac8a124cdb 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICryptScheme.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICryptScheme.java
@@ -30,8 +30,11 @@ import javax.crypto.SecretKey;
  * the authentication tag and cannot be altered to force a different scheme.
  * <p>
  * Schemes must use authenticated (AEAD) encryption: {@link #decrypt} returns 
{@code null} when
- * authentication fails. All keys are 256-bit AES {@link SecretKey}s generated 
by the factory;
- * schemes do not derive keys.
+ * authentication fails. A scheme also produces the {@link SecretKey}s it 
consumes (see
+ * {@link #generateKey(SecureRandom)}) &mdash; the key material is a property 
of the scheme, whereas
+ * a {@link ICryptFactory factory} only decides <em>where</em> the key lives 
(per session, global,
+ * externally supplied). All schemes sharing one {@link SchemeCrypt} must 
produce mutually
+ * compatible keys, since existing ciphertext is decrypted with the current 
key during migration.
  * <p>
  * Implementations must be thread-safe and pick a unique, stable {@link 
#id()}. Wicket reserves
  * ids {@code 1..31}; custom schemes should use ids {@code >= 32}.
@@ -43,6 +46,15 @@ public interface ICryptScheme
         */
        byte id();
 
+       /**
+        * Generate a new secret key suitable for this scheme.
+        *
+        * @param random
+        *            source of randomness
+        * @return a new, serializable {@link SecretKey}
+        */
+       SecretKey generateKey(SecureRandom random);
+
        /**
         * Encrypt the given plaintext.
         *
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionCryptFactory.java
 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionCryptFactory.java
index 2b9eea54ab..4daf79bea0 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionCryptFactory.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionCryptFactory.java
@@ -16,15 +16,11 @@
  */
 package org.apache.wicket.core.util.crypt;
 
-import java.security.SecureRandom;
-
 import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
 
 import org.apache.wicket.Application;
 import org.apache.wicket.MetaDataKey;
 import org.apache.wicket.Session;
-import org.apache.wicket.util.crypt.CipherUtils;
 
 /**
  * The default {@link ICryptFactory}: it generates a fresh 256-bit AES key per 
user session and
@@ -58,17 +54,4 @@ public class KeyInSessionCryptFactory extends 
AbstractCryptFactory
                }
                return key;
        }
-
-       /**
-        * Generates a new (serializable) 256-bit AES key.
-        *
-        * @param random
-        *            source of randomness
-        * @return a new key
-        */
-       protected SecretKey generateKey(SecureRandom random)
-       {
-               byte[] encoded = CipherUtils.generateKey("AES", 256, 
random).getEncoded();
-               return new SecretKeySpec(encoded, "AES");
-       }
 }
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 e0a714ebcf..0a0abe9a25 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
@@ -18,10 +18,8 @@ package org.apache.wicket.pageStore;
 
 import java.io.Serializable;
 import java.nio.ByteBuffer;
-import java.security.SecureRandom;
 
 import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
 
 import org.apache.wicket.Application;
 import org.apache.wicket.MetaDataKey;
@@ -30,7 +28,6 @@ import org.apache.wicket.core.util.crypt.ICrypt;
 import org.apache.wicket.core.util.crypt.SchemeCrypt;
 import org.apache.wicket.page.IManageablePage;
 import org.apache.wicket.settings.SecuritySettings;
-import org.apache.wicket.util.crypt.CipherUtils;
 import org.apache.wicket.util.lang.Args;
 
 /**
@@ -97,8 +94,9 @@ public class CryptingPageStore extends DelegatingPageStore
 
        private SecretKey generateKey()
        {
-               SecureRandom random = 
application.getSecuritySettings().getRandomSupplier().getRandom();
-               return new SecretKeySpec(CipherUtils.generateKey("AES", 256, 
random).getEncoded(), "AES");
+               SecuritySettings settings = application.getSecuritySettings();
+               // the scheme decides what key to generate; the store only 
decides where it lives (session)
+               return 
settings.getCryptScheme().generateKey(settings.getRandomSupplier().getRandom());
        }
 
        /**
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/settings/SecuritySettings.java 
b/wicket-core/src/main/java/org/apache/wicket/settings/SecuritySettings.java
index ed12b61b39..cde0f5c8bf 100644
--- a/wicket-core/src/main/java/org/apache/wicket/settings/SecuritySettings.java
+++ b/wicket-core/src/main/java/org/apache/wicket/settings/SecuritySettings.java
@@ -33,7 +33,6 @@ import java.util.Collection;
 import java.util.List;
 
 import javax.crypto.SecretKey;
-import javax.crypto.spec.SecretKeySpec;
 
 import org.apache.wicket.core.random.DefaultSecureRandomSupplier;
 import org.apache.wicket.core.random.ISecureRandomSupplier;
@@ -43,7 +42,6 @@ import org.apache.wicket.core.util.crypt.ICryptFactory;
 import org.apache.wicket.core.util.crypt.ICryptScheme;
 import org.apache.wicket.core.util.crypt.KeyInSessionCryptFactory;
 import org.apache.wicket.core.util.crypt.SchemeCrypt;
-import org.apache.wicket.util.crypt.CipherUtils;
 import org.apache.wicket.util.lang.Args;
 
 /**
@@ -360,8 +358,7 @@ public class SecuritySettings
                        // application-wide key. The key is random per boot; 
supply a custom strategy with a
                        // stable key to keep cookies valid across application 
restarts.
                        SecureRandom random = getRandomSupplier().getRandom();
-                       SecretKey key = new SecretKeySpec(
-                               CipherUtils.generateKey("AES", 256, 
random).getEncoded(), "AES");
+                       SecretKey key = getCryptScheme().generateKey(random);
                        ICrypt crypt = new SchemeCrypt(key, random, 
getCryptScheme(),
                                getWhitelistedCryptSchemes());
                        authenticationStrategy = new 
DefaultAuthenticationStrategy("LoggedIn", crypt);

Reply via email to