You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by pa...@apache.org on 2026/08/04 11:42:58 UTC
(wicket) branch crypt-unification updated (bafd478781 -> a1864aeaf6)
This is an automated email from the ASF dual-hosted git repository.
papegaaij pushed a change to branch crypt-unification
in repository https://gitbox.apache.org/repos/asf/wicket.git
discard bafd478781 Verify the upload token by decrypting it
discard 91d07cc72f Update the user guide for the redesigned crypt API
discard 7615036920 Let the crypt scheme generate its own key
discard 7faa1f7ab8 Bind encrypted pages to their page id via AEAD associated data
discard 156d391aab Unify and harden the cryptography subsystem (Wicket 11)
new 09e05c3d98 WICKET-7190 Unify and harden the cryptography subsystem (Wicket 11)
new 31b7ce12f6 WICKET-7190 Bind encrypted pages to their page id via AEAD associated data
new 6b85d7c81a WICKET-7190 Let the crypt scheme generate its own key
new 33eaf71733 WICKET-7190 Update the user guide for the redesigned crypt API
new e1c2f26508 WICKET-7190 Verify the upload token by decrypting it
new a1864aeaf6 WICKET-7190 State the page store trust boundary on the crypt API
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (bafd478781)
\
N -- N -- N refs/heads/crypt-unification (a1864aeaf6)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../main/java/org/apache/wicket/core/util/crypt/ICryptScheme.java | 5 +++++
.../main/java/org/apache/wicket/pageStore/CryptingPageStore.java | 6 ++++++
2 files changed, 11 insertions(+)
(wicket) 04/06: WICKET-7190 Update the user guide for the redesigned crypt API
Posted by pa...@apache.org.
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 33eaf71733eb3bfb2f71b99b618585db95cb69a4
Author: Emond Papegaaij <em...@topicus.nl>
AuthorDate: Fri Jul 3 16:04:56 2026 +0200
WICKET-7190 Update the user guide for the redesigned crypt API
The "URLs encryption in detail" section still described the Wicket 10 crypt API
(some of it already stale before this work). Bring it in line with the redesign:
- ICrypt is shown from its real package org.apache.wicket.core.util.crypt with
the byte-array + URL-safe-String signature and the null-on-failure contract.
- The default implementation is SchemeCrypt (self-describing, authenticated
ciphertext), not SunJceCrypt/PBEWithMD5AndDES. Document the pluggable
ICryptScheme, the default AesGcmCryptScheme (JDK-native AES-256-GCM), the
scheme whitelist / downgrade protection and migration, and the AES-256-GCM-SIV
opt-in (requires Bouncy Castle).
- The default factory is KeyInSessionCryptFactory (was KeyInSessionSunJceCrypt-
Factory); the stateless note now points to ApplicationKeyCryptFactory.
Also drop the outdated warning in the URL chapter that the default cipher "might
not be strong enough for production": the default is now authenticated
AES-256-GCM with a per-session key.
Co-Authored-By: Claude Opus 4.8 (1M context) <no...@anthropic.com>
---
.../src/main/asciidoc/security/security_4.adoc | 43 +++++++++++++++-------
.../src/main/asciidoc/urls/urls_6.adoc | 4 +-
2 files changed, 31 insertions(+), 16 deletions(-)
diff --git a/wicket-user-guide/src/main/asciidoc/security/security_4.adoc b/wicket-user-guide/src/main/asciidoc/security/security_4.adoc
index 17c5939628..8e7dbd644c 100644
--- a/wicket-user-guide/src/main/asciidoc/security/security_4.adoc
+++ b/wicket-user-guide/src/main/asciidoc/security/security_4.adoc
@@ -1,27 +1,45 @@
-In chapter <<_generating_structured_and_clear_urls,10.6>> we have seen how to encrypt URLs using _CryptoMapper_ request mapper. To encrypt/decrypt page URLs _CryptoMapper_ uses an instance of _org.apache.wicket.util.crypt.ICrypt_ interface:
+In chapter <<_generating_structured_and_clear_urls,10.6>> we have seen how to encrypt URLs using _CryptoMapper_ request mapper. To encrypt/decrypt page URLs _CryptoMapper_ uses an instance of the _org.apache.wicket.core.util.crypt.ICrypt_ interface:
[source,java]
----
public interface ICrypt
{
- String encryptUrlSafe(final String plainText);
+ byte[] encrypt(byte[] plainBytes, byte[] associatedData);
- String decryptUrlSafe(final String encryptedText);
+ byte[] decrypt(byte[] encryptedBytes, byte[] associatedData);
+
+ // URL-safe (Base64) String convenience layer used by CryptoMapper
+ default String encryptUrlSafe(String plainText) { ... }
+
+ default String decryptUrlSafe(String encryptedText) { ... }
...
}
----
-The default implementation for this interface is class _org.apache.wicket.util.crypt.SunJceCrypt_. It provides password-based cryptography using _PBEWithMD5AndDES_ algorithm coming with the standard security providers in the Java Runtime Environment.
+_ICrypt_ works on raw bytes and provides a URL-safe Base64 _String_ layer on top. The same interface is used everywhere Wicket encrypts data: encrypted URLs (_CryptoMapper_), the page store (see <<_page_storing,the chapter on page storing>>) and the "remember me" cookie. Decryption returns `null` on _any_ failure (unknown cipher, failed authentication, malformed input) instead of throwing, so undecryptable data is uniformly treated as absent.
-NOTE: Since Java 9, unlimited strength cryptography is enabled by default. No additional configuration is needed.
+The default implementation is _org.apache.wicket.core.util.crypt.SchemeCrypt_. It produces *self-describing, authenticated* ciphertext: every ciphertext is prefixed with a one-byte marker identifying the cipher (the "scheme") that produced it, and the payload is protected with authenticated encryption (AEAD) so any tampering is detected on decryption. The actual cipher is a pluggable _org.apache.wicket.core.util.crypt.ICryptScheme_; by default this is _AesGcmCryptScheme_, which uses JDK- [...]
-By using _CryptoMapper(IRequestMapper wrappedMapper, Application application)_ constructor the mapper will use the configured _org.apache.wicket.util.crypt.ICryptFactory_ from _org.apache.wicket.settings.SecuritySettings.getCryptFactory()_. To use a stronger cryptography mechanism there are the following options:
+NOTE: Since Java 9 unlimited strength cryptography is enabled by default, so AES-256 works out of the box with no additional configuration.
-* The first option is to use constructor _CryptoMapper(IRequestMapper wrappedMapper, Supplier<ICrypt> cryptProvider)_ and give it an implementation of _java.util.function.Supplier_ that returns a custom _org.apache.wicket.util.crypt.ICrypt_.
+The scheme is configured application-wide through _org.apache.wicket.settings.SecuritySettings_:
-* The second option is to register a cipher factory at application level with method _setCryptFactory(ICryptFactory cryptFactory)_ of class _SecuritySettings_:
+[source,java]
+----
+@Override
+public void init() {
+ super.init();
+ // switch to AES-256-GCM-SIV (nonce-misuse resistant); requires Bouncy Castle
+ getSecuritySettings().setCryptScheme(new AesGcmSivCryptScheme());
+ setRootRequestMapper(new CryptoMapper(getRootRequestMapper(), this));
+}
+----
+
+_AesGcmSivCryptScheme_ (AES-256-GCM-SIV) is also available but requires https://www.bouncycastle.org/[Bouncy Castle] on the classpath with its provider registered. On decryption, the scheme marker must match one of the schemes accepted by _SecuritySettings.setWhitelistedCryptSchemes_ (the encryption scheme is always accepted); unknown or non-whitelisted schemes are refused, which prevents downgrade attacks. To migrate to a stronger scheme without breaking existing data, temporarily white [...]
+
+While the scheme decides _how_ data is encrypted, an _org.apache.wicket.core.util.crypt.ICryptFactory_ decides _which key_ to use. The factory is configured with _SecuritySettings.setCryptFactory(ICryptFactory)_:
[source,java]
----
@@ -33,11 +51,8 @@ public void init() {
}
----
+The default factory is _org.apache.wicket.core.util.crypt.KeyInSessionCryptFactory_. It generates a unique, random 256-bit key for each user and stores it in her HTTP session. This way it helps to protect the application against https://owasp.org/www-community/attacks/csrf[CSRF] for each user of the application: the url itself serves as an https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html[encrypted token].
-Since version 6.19.0 Wicket uses _org.apache.wicket.core.util.crypt.KeyInSessionSunJceCryptFactory_ as a default factory for _ICrypt_ objects. This factory generates a unique key for each user that is stored in her HTTP
-session. This way it helps to protect the application against https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)[CSRF]
-for each user of the application. The url itself serves as https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Encrypted_Token_Pattern[encrypted token]
-
-WARNING: _org.apache.wicket.core.util.crypt.KeyInSessionSunJceCryptFactory_ binds the http session if it is not already bound! If the application needs to run in stateless mode then the application will have to provide a custom
-implementation of _ICryptFactory_ that stores the user specific keys by other means.
+WARNING: _KeyInSessionCryptFactory_ binds the HTTP session if it is not already bound! If the application needs to run in stateless mode it must provide a factory that does not rely on the session — for example _org.apache.wicket.core.util.crypt.ApplicationKeyCryptFactory_ with a stable, application-wide key. Note that an application-wide key means encrypted URLs are no longer isolated per user.
+Finally, _CryptoMapper_ offers two constructors: _CryptoMapper(IRequestMapper wrappedMapper, Application application)_ uses the _ICryptFactory_ configured on _SecuritySettings_, while _CryptoMapper(IRequestMapper wrappedMapper, Supplier<ICrypt> cryptProvider)_ lets you supply a specific _ICrypt_ instance directly.
diff --git a/wicket-user-guide/src/main/asciidoc/urls/urls_6.adoc b/wicket-user-guide/src/main/asciidoc/urls/urls_6.adoc
index 7f0ab3b16f..721ae8cee2 100644
--- a/wicket-user-guide/src/main/asciidoc/urls/urls_6.adoc
+++ b/wicket-user-guide/src/main/asciidoc/urls/urls_6.adoc
@@ -224,6 +224,6 @@ public void init() {
As pointed out in the code above, pages and resources must be mounted after having set _CryptoMapper_ as root mapper, otherwise the mounted paths will not work.
-WARNING: By default _CryptoMapper_ encrypts page URLs with a cipher that might not be strong enough for production environment. Paragraph
-<<_security_with_wicket,"Security with Wicket">> will provide a more detailed description of how Wicket encrypts page URLs and we will see how to use stronger ciphers.
+NOTE: By default _CryptoMapper_ encrypts page URLs with authenticated AES-256-GCM using a unique key per user session, which is suitable for production. Section
+<<_urls_encryption_in_detail,"URLs encryption in detail">> describes how Wicket encrypts URLs and how to configure the cipher (scheme) and the key source.
(wicket) 03/06: WICKET-7190 Let the crypt scheme generate its own key
Posted by pa...@apache.org.
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 <em...@topicus.nl>
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) <no...@anthropic.com>
---
.../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 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)}) — 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 — not the
+ * factory — 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 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)}) — 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);
(wicket) 06/06: WICKET-7190 State the page store trust boundary on the crypt API
Posted by pa...@apache.org.
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 a1864aeaf6687eb1bcade8c8b08a588b9ecdb6ab
Author: Emond Papegaaij <em...@topicus.nl>
AuthorDate: Tue Aug 4 13:34:23 2026 +0200
WICKET-7190 State the page store trust boundary on the crypt API
The redesigned crypt classes document what they guarantee, but not the limits
of that guarantee. Both points were made in the javadoc of the ICrypter
implementations this branch removes, and until now survived only in
StoreSettings#setEncrypted, the user guide and SECURITY.md - none of which a
reader lands on when they open CryptingPageStore or implement ICryptScheme.
Record on CryptingPageStore that the session-held key protects the stored
pages against a party who can read or write the underlying store, but not
against one who already controls the session, so the store is still trusted
storage. Note on ICryptScheme that a custom scheme returning unverified
plaintext instead of null removes the tamper detection callers rely on, and
point both at SECURITY.md.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
.../main/java/org/apache/wicket/core/util/crypt/ICryptScheme.java | 5 +++++
.../main/java/org/apache/wicket/pageStore/CryptingPageStore.java | 6 ++++++
2 files changed, 11 insertions(+)
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 ac8a124cdb..175c7be3e1 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
@@ -38,6 +38,11 @@ import javax.crypto.SecretKey;
* <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}.
+ * <p>
+ * A custom scheme that does not honour the AEAD contract - returning unverified plaintext instead
+ * of {@code null} on authentication failure - silently removes the tamper detection that callers
+ * rely on. See {@code SECURITY.md} for the trust assumptions Wicket makes about encrypted data,
+ * and {@link org.apache.wicket.pageStore.CryptingPageStore} for the page-store use of a scheme.
*/
public interface ICryptScheme
{
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 0a0abe9a25..44235c5b77 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
@@ -43,6 +43,12 @@ import org.apache.wicket.util.lang.Args;
* Each session gets its own random 256-bit AES key. Encryption uses the application's configured
* {@link SecuritySettings#getCryptScheme() crypt scheme}; a page that can no longer be decrypted
* (e.g. because the session key is gone or the data was tampered with) is treated as a cache miss.
+ * <p>
+ * Because the key lives in the session, the encryption protects the stored pages against a party
+ * who can read or write the underlying store - not against one who already controls the session.
+ * The store should therefore still be treated as trusted, private storage: Wicket deserializes
+ * whatever it reads back from it. See {@code SECURITY.md} for the trust assumptions Wicket makes
+ * about the page store.
*/
public class CryptingPageStore extends DelegatingPageStore
{
(wicket) 02/06: WICKET-7190 Bind encrypted pages to their page id via AEAD associated data
Posted by pa...@apache.org.
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 31b7ce12f69e321eb2652cac0c75aa751c727041
Author: Emond Papegaaij <em...@topicus.nl>
AuthorDate: Fri Jul 3 15:03:26 2026 +0200
WICKET-7190 Bind encrypted pages to their page id via AEAD associated data
Expose associated data (AAD) in the crypt API and use it to bind each encrypted
page to its id:
- ICrypt gains encrypt(byte[], byte[] associatedData) and
decrypt(byte[], byte[] associatedData); the no-arg overloads delegate with no
associated data. SchemeCrypt authenticates marker || associatedData, so the
no-associated-data path is unchanged and URL/cookie ciphertext is unaffected.
- CryptingPageStore passes the page id (4 big-endian bytes) as associated data,
using the trusted id parameter on getPage and the server-assigned id on
addPage. A blob stored for one id can no longer be substituted into another
slot by a tamper-capable storage adversary: it fails authentication and is
treated as a cache miss.
URLs (CryptoMapper) and the remember-me cookie bind no associated data: there is
no useful context to add (the session is redundant under a per-session key and
breaks URL sharing under an application-wide key; request-specific context would
break bookmarkability).
Co-Authored-By: Claude Opus 4.8 (1M context) <no...@anthropic.com>
---
.../wicket/core/util/crypt/SchemeCryptTest.java | 23 +++++++++++++
.../wicket/pageStore/CryptingPageStoreTest.java | 26 +++++++++++++++
.../CryptedUrlWebRequestCodingStrategyTest.java | 4 +--
.../org/apache/wicket/core/util/crypt/ICrypt.java | 39 ++++++++++++++++++++--
.../org/apache/wicket/core/util/crypt/NoCrypt.java | 4 +--
.../apache/wicket/core/util/crypt/SchemeCrypt.java | 27 +++++++++++----
.../apache/wicket/pageStore/CryptingPageStore.java | 15 +++++++--
7 files changed, 123 insertions(+), 15 deletions(-)
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 bde6c01054..a8b77552e3 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
@@ -118,6 +118,29 @@ public class SchemeCryptTest
assertNull(crypt(newKey(), scheme).decrypt(enc));
}
+ @ParameterizedTest
+ @MethodSource("schemes")
+ void associatedDataMustMatch(ICryptScheme scheme)
+ {
+ SchemeCrypt crypt = crypt(newKey(), scheme);
+ byte[] plain = "data".getBytes(StandardCharsets.UTF_8);
+ byte[] aad = { 1, 2, 3, 4 };
+
+ byte[] enc = crypt.encrypt(plain, aad);
+
+ // only the identical associated data decrypts
+ assertArrayEquals(plain, crypt.decrypt(enc, aad));
+ // a different, absent, or no-arg associated data fails authentication
+ assertNull(crypt.decrypt(enc, new byte[] { 9, 9, 9, 9 }));
+ assertNull(crypt.decrypt(enc, null));
+ assertNull(crypt.decrypt(enc));
+
+ // data encrypted without associated data cannot be read as if it had some, and vice versa
+ byte[] encNoAad = crypt.encrypt(plain);
+ assertArrayEquals(plain, crypt.decrypt(encNoAad));
+ assertNull(crypt.decrypt(encNoAad, aad));
+ }
+
@Test
void emptyOrShortInputReturnsNull()
{
diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java
index 5c6b763903..214036107d 100644
--- a/wicket-core-tests/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java
+++ b/wicket-core-tests/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java
@@ -119,6 +119,32 @@ public class CryptingPageStoreTest extends WicketTestCase
assertNull(store.getPage(context, p));
}
+ @ParameterizedTest
+ @MethodSource("schemes")
+ void pageIsBoundToItsId(ICryptScheme scheme)
+ {
+ MockPageStore delegate = new MockPageStore();
+ CryptingPageStore store = buildPageStore(scheme, delegate);
+ JavaSerializer serializer = new JavaSerializer("test");
+
+ MockPageContext context = new MockPageContext();
+
+ int n = 1;
+ int m = 2;
+ store.addPage(context, new SerializedPage(n, "foo", serializer.serialize(new MockPage(n))));
+
+ // copy page n's stored ciphertext into slot m
+ SerializedPage storedN = (SerializedPage) delegate.getPage(context, n);
+ delegate.addPage(context, new SerializedPage(m, "foo", storedN.getData()));
+
+ // reading slot m must fail: the blob is bound (via associated data) to id n
+ assertNull(store.getPage(context, m));
+
+ // page n itself still decrypts
+ SerializedPage got = (SerializedPage) store.getPage(context, n);
+ assertEquals(n, ((MockPage) serializer.deserialize(got.getData())).getPageId());
+ }
+
private CryptingPageStore buildPageStore(ICryptScheme scheme, MockPageStore delegate)
{
tester.getApplication()
diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/protocol/http/request/CryptedUrlWebRequestCodingStrategyTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/protocol/http/request/CryptedUrlWebRequestCodingStrategyTest.java
index 0c60aeeb11..036c0fce76 100644
--- a/wicket-core-tests/src/test/java/org/apache/wicket/protocol/http/request/CryptedUrlWebRequestCodingStrategyTest.java
+++ b/wicket-core-tests/src/test/java/org/apache/wicket/protocol/http/request/CryptedUrlWebRequestCodingStrategyTest.java
@@ -109,13 +109,13 @@ class CryptedUrlWebRequestCodingStrategyTest extends WicketTestCase
return new ICrypt()
{
@Override
- public byte[] encrypt(byte[] plainBytes)
+ public byte[] encrypt(byte[] plainBytes, byte[] associatedData)
{
return plainBytes;
}
@Override
- public byte[] decrypt(byte[] encryptedBytes)
+ public byte[] decrypt(byte[] encryptedBytes, byte[] associatedData)
{
return encryptedBytes;
}
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICrypt.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICrypt.java
index 76852ddb87..3a84f4551f 100644
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICrypt.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICrypt.java
@@ -40,19 +40,52 @@ public interface ICrypt
*
* @param plainBytes
* the bytes to encrypt, must not be {@code null}
+ * @param associatedData
+ * optional additional data that is authenticated but not encrypted; the identical
+ * value must be supplied to {@link #decrypt(byte[], byte[])} or decryption fails.
+ * Use it to bind the ciphertext to its context (e.g. a page id). May be
+ * {@code null}.
* @return the encrypted bytes
*/
- byte[] encrypt(byte[] plainBytes);
+ byte[] encrypt(byte[] plainBytes, byte[] associatedData);
/**
* Decrypt the given bytes.
*
* @param encryptedBytes
* the bytes to decrypt
+ * @param associatedData
+ * the same additional data that was supplied to {@link #encrypt(byte[], byte[])},
+ * or {@code null} if none was used
* @return the decrypted bytes, or {@code null} if the input could not be decrypted for any
- * reason (unknown or non-whitelisted scheme, failed authentication, malformed input)
+ * reason (unknown or non-whitelisted scheme, failed authentication including an
+ * associated-data mismatch, malformed input)
*/
- byte[] decrypt(byte[] encryptedBytes);
+ byte[] decrypt(byte[] encryptedBytes, byte[] associatedData);
+
+ /**
+ * Encrypt the given bytes without additional associated data.
+ *
+ * @param plainBytes
+ * the bytes to encrypt, must not be {@code null}
+ * @return the encrypted bytes
+ */
+ default byte[] encrypt(byte[] plainBytes)
+ {
+ return encrypt(plainBytes, null);
+ }
+
+ /**
+ * Decrypt the given bytes that were encrypted without additional associated data.
+ *
+ * @param encryptedBytes
+ * the bytes to decrypt
+ * @return the decrypted bytes, or {@code null} if the input could not be decrypted
+ */
+ default byte[] decrypt(byte[] encryptedBytes)
+ {
+ return decrypt(encryptedBytes, null);
+ }
/**
* Encrypt the given text into a URL-safe (Base64, no padding) {@code String}.
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/NoCrypt.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/NoCrypt.java
index 1fa3a992ec..81e4281a08 100644
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/NoCrypt.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/NoCrypt.java
@@ -27,13 +27,13 @@ package org.apache.wicket.core.util.crypt;
public class NoCrypt implements ICrypt
{
@Override
- public byte[] encrypt(byte[] plainBytes)
+ public byte[] encrypt(byte[] plainBytes, byte[] associatedData)
{
return plainBytes;
}
@Override
- public byte[] decrypt(byte[] encryptedBytes)
+ public byte[] decrypt(byte[] encryptedBytes, byte[] associatedData)
{
return encryptedBytes;
}
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/SchemeCrypt.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/SchemeCrypt.java
index d89a9a2bd6..4602b0867a 100644
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/SchemeCrypt.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/SchemeCrypt.java
@@ -80,13 +80,12 @@ public class SchemeCrypt implements ICrypt
}
@Override
- public byte[] encrypt(byte[] plainBytes)
+ public byte[] encrypt(byte[] plainBytes, byte[] associatedData)
{
Args.notNull(plainBytes, "plainBytes");
byte id = encryptionScheme.id();
- byte[] aad = { id };
- byte[] payload = encryptionScheme.encrypt(plainBytes, key, aad, random);
+ byte[] payload = encryptionScheme.encrypt(plainBytes, key, aad(id, associatedData), random);
byte[] result = new byte[payload.length + 1];
result[0] = id;
@@ -95,7 +94,7 @@ public class SchemeCrypt implements ICrypt
}
@Override
- public byte[] decrypt(byte[] encryptedBytes)
+ public byte[] decrypt(byte[] encryptedBytes, byte[] associatedData)
{
if (encryptedBytes == null || encryptedBytes.length < 1)
{
@@ -110,8 +109,24 @@ public class SchemeCrypt implements ICrypt
return null;
}
- byte[] aad = { id };
byte[] payload = Arrays.copyOfRange(encryptedBytes, 1, encryptedBytes.length);
- return scheme.decrypt(payload, key, aad);
+ return scheme.decrypt(payload, key, aad(id, associatedData));
+ }
+
+ /**
+ * Builds the AEAD associated data by prefixing the caller-supplied associated data with the
+ * scheme marker. With no associated data this is just {@code {marker}} (unchanged behavior).
+ */
+ private static byte[] aad(byte marker, byte[] associatedData)
+ {
+ if (associatedData == null || associatedData.length == 0)
+ {
+ return new byte[] { marker };
+ }
+
+ byte[] aad = new byte[associatedData.length + 1];
+ aad[0] = marker;
+ System.arraycopy(associatedData, 0, aad, 1, associatedData.length);
+ return aad;
}
}
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 9860db1a67..e0a714ebcf 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
@@ -17,6 +17,7 @@
package org.apache.wicket.pageStore;
import java.io.Serializable;
+import java.nio.ByteBuffer;
import java.security.SecureRandom;
import javax.crypto.SecretKey;
@@ -126,7 +127,9 @@ public class CryptingPageStore extends DelegatingPageStore
SerializedPage serializedPage = (SerializedPage) page;
byte[] encrypted = serializedPage.getData();
- byte[] decrypted = getCrypt(context).decrypt(encrypted);
+ // bind the page to the id we are looking up (the trusted method parameter), so a
+ // blob that was stored for a different id fails authentication
+ byte[] decrypted = getCrypt(context).decrypt(encrypted, aad(id));
if (decrypted == null)
{
@@ -152,13 +155,21 @@ public class CryptingPageStore extends DelegatingPageStore
SerializedPage serializedPage = (SerializedPage) page;
byte[] decrypted = serializedPage.getData();
- byte[] encrypted = getCrypt(context).encrypt(decrypted);
+ byte[] encrypted = getCrypt(context).encrypt(decrypted, aad(serializedPage.getPageId()));
page = new SerializedPage(page.getPageId(), serializedPage.getPageType(), encrypted);
getDelegate().addPage(context, page);
}
+ /**
+ * The associated data binding an encrypted page to its id: the page id as 4 big-endian bytes.
+ */
+ private static byte[] aad(int pageId)
+ {
+ return ByteBuffer.allocate(Integer.BYTES).putInt(pageId).array();
+ }
+
private static class SessionData implements Serializable
{
private static final long serialVersionUID = 1L;
(wicket) 01/06: WICKET-7190 Unify and harden the cryptography subsystem (Wicket 11)
Posted by pa...@apache.org.
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 09e05c3d989cbb0642224ddc7a38abe940ef0f6d
Author: Emond Papegaaij <em...@topicus.nl>
AuthorDate: Fri Jul 3 14:03:05 2026 +0200
WICKET-7190 Unify and harden the cryptography subsystem (Wicket 11)
Replace the two disconnected crypt stacks (util.crypt.ICrypt and
pageStore.crypt.ICrypter) with a single authenticated, self-describing crypto
system in org.apache.wicket.core.util.crypt:
- ICryptScheme with AesGcmCryptScheme (JDK-native AES-256-GCM, the default) and
AesGcmSivCryptScheme (AES-256-GCM-SIV, Bouncy Castle, optional). Ciphertext is
prefixed with a one-byte scheme marker that is bound as AEAD associated data.
- SchemeCrypt selects the scheme by its marker against a configurable whitelist
(downgrade protection), always encrypts with the strongest configured scheme,
and returns null on any decryption failure.
- Key-source factories: KeyInSessionCryptFactory (per-session key, the default)
and ApplicationKeyCryptFactory (application-wide key, used by remember-me).
- SecuritySettings owns the crypto policy (crypt scheme + whitelist); the page
store, CryptoMapper and the remember-me cookie all use it.
This fixes the reported page-store weakness (unauthenticated AES-CBC, malleable
via CBC bit-flipping and vulnerable to a padding oracle) by making all
encryption authenticated, and removes the weak PBEWithMD5AndDES default that was
used for URLs and the authentication cookie.
Authenticated encryption uses a random nonce and is therefore non-deterministic.
CryptoMapper reuses, within a request, the ciphertext it decrypted so that a
regenerated URL matches the requested one; this avoids Wicket's URL-normalisation
infinite redirect while keeping random nonces (a nonce is only ever reused for
the identical plaintext).
Removes the obsolete implementations (SunJceCrypt, AbstractCrypt, TrivialCrypt,
CryptFactoryCachingDecorator, AESCrypt, AbstractJceCrypt,
KeyInSessionSunJceCryptFactory, AbstractKeyInSessionCryptFactory, DefaultCrypter,
GCMSIVCrypter, ICrypter) and adds a MigrateToWicket11 OpenRewrite recipe. Data
encrypted by Wicket 10 cannot be decrypted by Wicket 11 (clean break); the
marker/whitelist mechanism enables future migrations.
Co-Authored-By: Claude Opus 4.8 (1M context) <no...@anthropic.com>
---
SECURITY.md | 25 +-
.../core/request/mapper/CryptoMapperTest.java | 131 ++++++--
.../wicket/core/util/crypt/AESCryptTest.java | 54 ----
.../wicket/core/util/crypt/CryptFactoryTest.java | 60 ++++
.../wicket/core/util/crypt/SchemeCryptTest.java | 193 ++++++++++++
.../markup/html/form/encryption/CryptTest.java | 36 +--
.../wicket/pageStore/CryptingPageStoreTest.java | 86 +++---
.../CryptedUrlWebRequestCodingStrategyTest.java | 17 +-
wicket-core/pom.xml | 1 -
wicket-core/src/main/java/module-info.java | 1 -
.../strategy/DefaultAuthenticationStrategy.java | 12 +-
.../wicket/core/request/mapper/CryptoMapper.java | 111 ++++++-
.../apache/wicket/core/util/crypt/AESCrypt.java | 132 --------
.../core/util/crypt/AbstractCryptFactory.java | 59 ++++
.../wicket/core/util/crypt/AbstractJceCrypt.java | 93 ------
.../crypt/AbstractKeyInSessionCryptFactory.java | 86 ------
.../wicket/core/util/crypt/AesGcmCryptScheme.java | 121 ++++++++
.../core/util/crypt/AesGcmSivCryptScheme.java | 121 ++++++++
.../util/crypt/ApplicationKeyCryptFactory.java | 66 ++++
.../org/apache/wicket/core/util/crypt/ICrypt.java | 95 ++++++
.../wicket/core}/util/crypt/ICryptFactory.java | 12 +-
.../wicket/core/util/crypt/ICryptScheme.java | 74 +++++
.../core/util/crypt/KeyInSessionCryptFactory.java | 74 +++++
.../util/crypt/KeyInSessionSunJceCryptFactory.java | 115 -------
.../org/apache/wicket/core/util/crypt/NoCrypt.java | 49 ++-
.../wicket/core}/util/crypt/NoCryptFactory.java | 11 +-
.../apache/wicket/core/util/crypt/SchemeCrypt.java | 117 +++++++
.../apache/wicket/pageStore/CryptingPageStore.java | 344 +++++++++++----------
.../wicket/pageStore/crypt/DefaultCrypter.java | 115 -------
.../wicket/pageStore/crypt/GCMSIVCrypter.java | 118 -------
.../apache/wicket/pageStore/crypt/ICrypter.java | 46 ---
.../apache/wicket/settings/SecuritySettings.java | 105 ++++++-
.../org/apache/wicket/settings/StoreSettings.java | 45 +--
.../wicket/examples/WicketExampleApplication.java | 2 +-
.../src/main/resources/META-INF/rewrite/wicket.yml | 41 +++
.../src/main/asciidoc/internals/pagestoring.adoc | 23 +-
.../apache/wicket/util/crypt/AbstractCrypt.java | 179 -----------
.../java/org/apache/wicket/util/crypt/ICrypt.java | 53 ----
.../java/org/apache/wicket/util/crypt/NoCrypt.java | 62 ----
.../org/apache/wicket/util/crypt/SunJceCrypt.java | 176 -----------
.../org/apache/wicket/util/crypt/TrivialCrypt.java | 38 ---
.../apache/wicket/util/crypt/SunJceCryptTest.java | 74 -----
42 files changed, 1631 insertions(+), 1742 deletions(-)
diff --git a/SECURITY.md b/SECURITY.md
index 1a152a6476..41e2945eb2 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -158,13 +158,24 @@ session store as trusted, private storage: do not point them at storage that
untrusted parties can write to, and do not accept externally supplied
serialized page or session data.
-The exception is a page store configured with encryption
-(`StoreSettings#setEncrypted(true)`), and then only as far as the configured
-`ICrypter` implementation documents. Encryption does not by itself imply
-tamper detection: whether the stored bytes are merely confidential or are also
-protected against modification depends entirely on the implementation in use,
-and the default implementation is not authenticated. Consult the javadoc of the
-`ICrypter` you configure and rely on no more than it states.
+The partial exception is a page store configured with encryption
+(`StoreSettings#setEncrypted(true)`). Every `ICryptScheme` Wicket ships is
+authenticated (AEAD), so encrypted pages are tamper-evident as well as
+confidential: modified or substituted bytes fail to decrypt and the page is
+treated as absent instead of being handed to the deserializer. Each page is
+additionally bound to the page id it was stored under, so a stored page cannot
+be replayed as a different one. The scheme marker prefixing each ciphertext is
+authenticated too, and is refused unless it is one of the schemes accepted by
+`SecuritySettings#setWhitelistedCryptSchemes`, so an attacker cannot force
+decryption with a weaker scheme.
+
+Three limits on that exception are worth stating. The key lives in the user's
+session, so this protects the stored pages against a party who can read or
+write the store, not against one who already controls the session. It covers
+the page store only — the container's session store, and anything else holding
+serialized Wicket data, remains trusted storage. And a custom `ICryptScheme`
+inherits the guarantee only if it honours the contract: `decrypt` must return
+`null` on authentication failure rather than returning unverified plaintext.
### Another origin may not invoke a listener
diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/core/request/mapper/CryptoMapperTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/core/request/mapper/CryptoMapperTest.java
index 41ab1c2e6c..2568869a3d 100644
--- a/wicket-core-tests/src/test/java/org/apache/wicket/core/request/mapper/CryptoMapperTest.java
+++ b/wicket-core-tests/src/test/java/org/apache/wicket/core/request/mapper/CryptoMapperTest.java
@@ -18,12 +18,20 @@ package org.apache.wicket.core.request.mapper;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.security.SecureRandom;
+import java.util.List;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
import org.apache.wicket.MockPage;
import org.apache.wicket.core.request.handler.BookmarkableListenerRequestHandler;
import org.apache.wicket.core.request.handler.ListenerRequestHandler;
@@ -48,8 +56,9 @@ import org.apache.wicket.request.mapper.info.PageComponentInfo;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.resource.PackageResourceReference;
import org.apache.wicket.request.resource.UrlResourceReference;
-import org.apache.wicket.util.crypt.ICrypt;
-import org.apache.wicket.util.crypt.SunJceCrypt;
+import org.apache.wicket.core.util.crypt.AesGcmCryptScheme;
+import org.apache.wicket.core.util.crypt.ICrypt;
+import org.apache.wicket.core.util.crypt.SchemeCrypt;
import org.apache.wicket.util.string.StringValue;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.tester.WicketTester;
@@ -64,13 +73,13 @@ class CryptoMapperTest extends AbstractMapperTest
{
private static final String PLAIN_BOOKMARKABLE_URL = "wicket/bookmarkable/" +
Page2.class.getName();
- private static final String ENCRYPTED_BOOKMARKABLE_URL = "L7ExSNbPC4sb6TPJDblCAopL53TWmZP5y7BQEaJSJAC05HXod5M5U7gT2yNT0lK5L6L09ZAOoZkGyUhseyPrC4S5tqUUrV6zipc4_Ni877EmwR8AyCyA-A/L7E59/5y7f2";
private static final String PLAIN_PAGE_INSTANCE_URL = "wicket/page?5";
- private static final String ENCRYPTED_PAGE_INSTANCE_URL = "fyBfZ9p6trOhokHCzsQS6Q/fyBce";
private static final String MOUNTED_URL = "path/to/mounted/page";
private CryptoMapper mapper;
+ private ICrypt crypt;
+
private WicketTester tester;
private static IRequestHandler unwrapRequestHandlerDelegate(IRequestHandler handler)
@@ -96,9 +105,17 @@ class CryptoMapperTest extends AbstractMapperTest
WebApplication application = tester.getApplication();
application.mountPage(MOUNTED_URL, Page1.class);
- SunJceCrypt crypt = new SunJceCrypt(new byte[]{ (byte)0x15, (byte)0x8c, (byte)0xa3, (byte)0x4a,
- (byte)0x66, (byte)0x51, (byte)0x2a, (byte)0xbc }, 17);
- crypt.setKey("WiCkEt-FRAMEwork");
+ // a fixed-key authenticated crypt keeps the tests self-contained; note that
+ // authenticated encryption uses a random nonce, so ciphertext is not deterministic and
+ // tests must round-trip (encrypt then decrypt) rather than assert exact encrypted strings
+ byte[] keyBytes = new byte[32];
+ for (int i = 0; i < keyBytes.length; i++)
+ {
+ keyBytes[i] = (byte)i;
+ }
+ SecretKey key = new SecretKeySpec(keyBytes, "AES");
+ AesGcmCryptScheme scheme = new AesGcmCryptScheme();
+ crypt = new SchemeCrypt(key, new SecureRandom(), scheme, List.of(scheme));
mapper = new CryptoMapper(application.getRootRequestMapper(), () -> crypt);
}
@@ -188,7 +205,15 @@ class CryptoMapperTest extends AbstractMapperTest
assertEquals(PLAIN_BOOKMARKABLE_URL, plainTextUrl.toString());
Url encryptedUrl = mapper.mapHandler(renderPage2BookmarkableHandler);
- assertEquals(ENCRYPTED_BOOKMARKABLE_URL, encryptedUrl.toString());
+ assertNotEquals(PLAIN_BOOKMARKABLE_URL, encryptedUrl.toString());
+ assertFalse(encryptedUrl.getSegments().isEmpty());
+ assertNotEquals("wicket", encryptedUrl.getSegments().get(0));
+
+ // the encrypted url must round-trip back to the same page
+ IRequestHandler requestHandler = unwrapRequestHandlerDelegate(
+ mapper.mapRequest(getRequest(encryptedUrl)));
+ assertThat(requestHandler).isInstanceOf(RenderPageRequestHandler.class);
+ assertEquals(Page2.class, ((RenderPageRequestHandler)requestHandler).getPageClass());
}
/**
@@ -198,7 +223,10 @@ class CryptoMapperTest extends AbstractMapperTest
@Test
void bookmarkablePageDecrypt()
{
- Request request = getRequest(Url.parse(ENCRYPTED_BOOKMARKABLE_URL));
+ Url encryptedUrl = mapper.mapHandler(
+ new RenderPageRequestHandler(new PageProvider(Page2.class, new PageParameters())));
+
+ Request request = getRequest(encryptedUrl);
IRequestHandler requestHandler = mapper.mapRequest(request);
assertNotNull(requestHandler);
@@ -219,9 +247,12 @@ class CryptoMapperTest extends AbstractMapperTest
@Test
void bookmarkablePageDecrypt2()
{
- String encryptedExtraSegments = "/i87b7/i87b7";
- Request request = getRequest(
- Url.parse(ENCRYPTED_BOOKMARKABLE_URL + encryptedExtraSegments));
+ Url encryptedUrl = mapper.mapHandler(
+ new RenderPageRequestHandler(new PageProvider(Page2.class, new PageParameters())));
+ // extra trailing (checksum) segments must be ignored
+ encryptedUrl.getSegments().add("i87b7");
+ encryptedUrl.getSegments().add("i87b7");
+ Request request = getRequest(encryptedUrl);
IRequestHandler requestHandler = mapper.mapRequest(request);
assertNotNull(requestHandler);
@@ -240,7 +271,9 @@ class CryptoMapperTest extends AbstractMapperTest
@Test
void bookmarkablePageDecryptMultipleCryptoMapper()
{
- Request request = getRequest(Url.parse(ENCRYPTED_BOOKMARKABLE_URL));
+ Url encryptedUrl = mapper.mapHandler(
+ new RenderPageRequestHandler(new PageProvider(Page2.class, new PageParameters())));
+ Request request = getRequest(encryptedUrl);
IRequestHandler requestHandler = new CryptoMapper(mapper, tester.getApplication())
.mapRequest(request);
@@ -254,6 +287,42 @@ class CryptoMapperTest extends AbstractMapperTest
assertEquals(Page2.class, handler.getPageClass());
}
+ /**
+ * Authenticated encryption uses a random nonce, so encrypting the same URL twice yields
+ * different ciphertext. To avoid Wicket's URL normalisation redirecting endlessly, the mapper
+ * reuses within a request the ciphertext it has already seen for a given plaintext: after
+ * decrypting an incoming URL, re-encrypting that same page reproduces exactly the URL that was
+ * requested, so {@code targetUrl == currentUrl} and no redirect happens. This test documents
+ * that behavior.
+ */
+ @Test
+ void encryptedUrlIsReusedWithinRequestToStayStable()
+ {
+ // the underlying authenticated crypt is non-deterministic (fresh random nonce each time)
+ assertNotEquals(crypt.encryptUrlSafe(PLAIN_BOOKMARKABLE_URL),
+ crypt.encryptUrlSafe(PLAIN_BOOKMARKABLE_URL));
+
+ // a properly-formed encrypted URL for a bookmarkable page, as a previous response produced
+ Url encrypted = mapper.mapHandler(
+ new RenderPageRequestHandler(new PageProvider(Page2.class, new PageParameters())));
+
+ // start a fresh request (with an empty per-request cache), simulating the client coming
+ // back with that URL
+ tester.startPage(HomePage.class);
+
+ // decrypting the incoming URL teaches the mapper the plaintext -> ciphertext mapping...
+ IRequestHandler requestHandler = mapper.mapRequest(getRequest(encrypted));
+ assertNotNull(requestHandler);
+ assertEquals(Page2.class, ((RenderPageRequestHandler) unwrapRequestHandlerDelegate(
+ requestHandler)).getPageClass());
+
+ // ...so re-encrypting the same page reproduces exactly the requested URL (nonce reuse),
+ // which is what stops the infinite redirect
+ Url regenerated = mapper.mapHandler(
+ new RenderPageRequestHandler(new PageProvider(Page2.class, new PageParameters())));
+ assertEquals(encrypted.toString(), regenerated.toString());
+ }
+
/**
* Tests that plain text URLs to bookmarkable pages are not mapped.
*/
@@ -292,7 +361,15 @@ class CryptoMapperTest extends AbstractMapperTest
assertEquals(PLAIN_PAGE_INSTANCE_URL,
mapper.getDelegateMapper().mapHandler(requestHandler).toString());
- assertEquals(ENCRYPTED_PAGE_INSTANCE_URL, mapper.mapHandler(requestHandler).toString());
+
+ Url encryptedUrl = mapper.mapHandler(requestHandler);
+ assertNotEquals(PLAIN_PAGE_INSTANCE_URL, encryptedUrl.toString());
+
+ // the encrypted url must round-trip back to the same page instance
+ IRequestHandler rt = unwrapRequestHandlerDelegate(
+ mapper.mapRequest(getRequest(encryptedUrl)));
+ assertThat(rt).isInstanceOf(RenderPageRequestHandler.class);
+ assertEquals(5, ((RenderPageRequestHandler)rt).getPageId().intValue());
}
/**
@@ -301,8 +378,9 @@ class CryptoMapperTest extends AbstractMapperTest
@Test
void pageInstanceDecrypt()
{
- IRequestHandler requestHandler = mapper
- .mapRequest(getRequest(Url.parse(ENCRYPTED_PAGE_INSTANCE_URL)));
+ Url encryptedUrl = mapper
+ .mapHandler(new RenderPageRequestHandler(new PageProvider(new MockPage(5))));
+ IRequestHandler requestHandler = mapper.mapRequest(getRequest(encryptedUrl));
assertNotNull(requestHandler);
requestHandler = unwrapRequestHandlerDelegate(requestHandler);
@@ -318,8 +396,10 @@ class CryptoMapperTest extends AbstractMapperTest
@Test
void pageInstanceDecryptMultipleCryptoMapper()
{
+ Url encryptedUrl = mapper
+ .mapHandler(new RenderPageRequestHandler(new PageProvider(new MockPage(5))));
IRequestHandler requestHandler = new CryptoMapper(mapper, tester.getApplication())
- .mapRequest(getRequest(Url.parse(ENCRYPTED_PAGE_INSTANCE_URL)));
+ .mapRequest(getRequest(encryptedUrl));
assertNotNull(requestHandler);
requestHandler = unwrapRequestHandlerDelegate(requestHandler);
@@ -440,14 +520,18 @@ class CryptoMapperTest extends AbstractMapperTest
int delegateBookmarkableScore = mapper.getDelegateMapper()
.getCompatibilityScore(getRequest(Url.parse(PLAIN_BOOKMARKABLE_URL)));
+ Url encryptedBookmarkableUrl = mapper.mapHandler(
+ new RenderPageRequestHandler(new PageProvider(Page2.class, new PageParameters())));
int cryptoBookmarkableScore = mapper
- .getCompatibilityScore(getRequest(Url.parse(ENCRYPTED_BOOKMARKABLE_URL)));
+ .getCompatibilityScore(getRequest(encryptedBookmarkableUrl));
assertEquals(delegateBookmarkableScore, cryptoBookmarkableScore);
int delegatePageInstanceScore = mapper.getDelegateMapper()
.getCompatibilityScore(getRequest(Url.parse(PLAIN_PAGE_INSTANCE_URL)));
+ Url encryptedPageInstanceUrl = mapper
+ .mapHandler(new RenderPageRequestHandler(new PageProvider(new MockPage(5))));
int cryptoPageInstanceScore = mapper
- .getCompatibilityScore(getRequest(Url.parse(ENCRYPTED_PAGE_INSTANCE_URL)));
+ .getCompatibilityScore(getRequest(encryptedPageInstanceUrl));
assertEquals(delegatePageInstanceScore, cryptoPageInstanceScore);
}
@@ -470,8 +554,6 @@ class CryptoMapperTest extends AbstractMapperTest
@Test
void pageParameters()
{
- String expectedEncrypted = "L7ExSNbPC4sb6TPJDblCAopL53TWmZP5y7BQEaJSJAC05HXod5M5U7gT2yNT0lK5L6L09ZAOoZkGyUhseyPrC4S5tqUUrV6zipc4_Ni877FDOOoE5C_Cd7YCyK1xSScpVhno6LeBz2wiu5oWyf7hB1RKcv6zkhEBmbx8vU7K7-e4xe1_LO8Y3fhEjMSQyU9BVh7Uz4HKzkR2OxFo5LaDzQ/L7E59/yPr6a/5L6ae/OxF2c";
-
PageParameters expectedParameters = new PageParameters();
expectedParameters.add("namedKey1", "namedValue1");
expectedParameters.add("namedKey2", "namedValue2");
@@ -480,7 +562,7 @@ class CryptoMapperTest extends AbstractMapperTest
RenderPageRequestHandler renderPageRequestHandler = new RenderPageRequestHandler(
new PageProvider(Page2.class, expectedParameters));
Url url = mapper.mapHandler(renderPageRequestHandler);
- assertEquals(expectedEncrypted, url.toString());
+ assertNotEquals(PLAIN_BOOKMARKABLE_URL, url.toString());
Request request = getRequest(url);
IRequestHandler requestHandler = mapper.mapRequest(request);
@@ -637,7 +719,10 @@ class CryptoMapperTest extends AbstractMapperTest
void markedEncryptedUrlDecrypt()
{
mapper.setMarkEncryptedUrls(true);
- Request request = getRequest(Url.parse("crypt." + ENCRYPTED_BOOKMARKABLE_URL));
+ Url encryptedUrl = mapper.mapHandler(
+ new RenderPageRequestHandler(new PageProvider(Page2.class, new PageParameters())));
+ assertTrue(encryptedUrl.getSegments().get(0).startsWith("crypt."));
+ Request request = getRequest(encryptedUrl);
IRequestHandler requestHandler = mapper.mapRequest(request);
assertNotNull(requestHandler);
diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/AESCryptTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/AESCryptTest.java
deleted file mode 100644
index c88de9c2a5..0000000000
--- a/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/AESCryptTest.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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.core.util.crypt;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.apache.wicket.core.random.DefaultSecureRandomSupplier;
-import org.apache.wicket.util.crypt.CipherUtils;
-import org.junit.jupiter.api.Test;
-
-import java.security.GeneralSecurityException;
-
-import javax.crypto.SecretKey;
-
-public class AESCryptTest
-{
- @Test
- public void encrypDecrypt() throws GeneralSecurityException
- {
- DefaultSecureRandomSupplier randomSupplier = new DefaultSecureRandomSupplier();
-
- SecretKey secretKey = CipherUtils.generatePBEKey(
- "myWeakPassword", "PBKDF2WithHmacSHA1", "AES",
- randomSupplier.getRandomBytes(16), 65536, 256);
-
- AbstractJceCrypt crypt = new AESCrypt(secretKey, randomSupplier);
-
- String inputTest = "inputTest";
- String encrypted = crypt.encryptUrlSafe(inputTest);
-
- String japFlowerBirdsWindMoon = "花鳥風月";
- String encrypted2 = crypt.encryptUrlSafe(japFlowerBirdsWindMoon);
-
- String decrypted = crypt.decryptUrlSafe(encrypted);
- assertEquals(decrypted, inputTest);
-
- String decrypted2 = crypt.decryptUrlSafe(encrypted2);
- assertEquals(decrypted2, japFlowerBirdsWindMoon);
- }
-}
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
new file mode 100644
index 0000000000..0bea8d4ccc
--- /dev/null
+++ b/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/CryptFactoryTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.core.util.crypt;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.wicket.util.tester.WicketTestCase;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for the key-source {@link ICryptFactory factories}.
+ */
+class CryptFactoryTest extends WicketTestCase
+{
+ /**
+ * An application-wide key can decrypt data across separate {@link ICrypt} instances (as needed
+ * for e.g. a "remember me" cookie).
+ */
+ @Test
+ void applicationKeyIsStableAcrossCryptInstances()
+ {
+ SecretKey key = new SecretKeySpec(new byte[32], "AES");
+ ApplicationKeyCryptFactory factory = new ApplicationKeyCryptFactory(key);
+
+ 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.
+ */
+ @Test
+ void keyInSessionIsStableWithinSession()
+ {
+ tester.getSession().bind();
+ KeyInSessionCryptFactory factory = new KeyInSessionCryptFactory();
+
+ byte[] encrypted = factory.newCrypt().encrypt("hello".getBytes(UTF_8));
+ assertArrayEquals("hello".getBytes(UTF_8), factory.newCrypt().decrypt(encrypted));
+ }
+}
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
new file mode 100644
index 0000000000..bde6c01054
--- /dev/null
+++ b/wicket-core-tests/src/test/java/org/apache/wicket/core/util/crypt/SchemeCryptTest.java
@@ -0,0 +1,193 @@
+/*
+ * 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.core.util.crypt;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for {@link SchemeCrypt} and the {@link ICryptScheme} implementations, covering
+ * round-trip, tamper detection, downgrade protection (marker whitelist + authenticated marker)
+ * and scheme migration.
+ */
+public class SchemeCryptTest
+{
+ private static final SecureRandom RANDOM = new SecureRandom();
+
+ @BeforeAll
+ static void init()
+ {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ static List<ICryptScheme> schemes()
+ {
+ return List.of(new AesGcmCryptScheme(), new AesGcmSivCryptScheme());
+ }
+
+ private static SecretKey newKey()
+ {
+ byte[] bytes = new byte[32];
+ RANDOM.nextBytes(bytes);
+ return new SecretKeySpec(bytes, "AES");
+ }
+
+ private static SchemeCrypt crypt(SecretKey key, ICryptScheme scheme)
+ {
+ return new SchemeCrypt(key, RANDOM, scheme, List.of(scheme));
+ }
+
+ @ParameterizedTest
+ @MethodSource("schemes")
+ void roundTrip(ICryptScheme scheme)
+ {
+ SchemeCrypt crypt = crypt(newKey(), scheme);
+ byte[] plain = "the quick brown fox 花鳥風月".getBytes(StandardCharsets.UTF_8);
+ assertArrayEquals(plain, crypt.decrypt(crypt.encrypt(plain)));
+ }
+
+ @ParameterizedTest
+ @MethodSource("schemes")
+ void ciphertextStartsWithSchemeMarker(ICryptScheme scheme)
+ {
+ byte[] enc = crypt(newKey(), scheme).encrypt("x".getBytes(StandardCharsets.UTF_8));
+ assertEquals(scheme.id(), enc[0]);
+ }
+
+ @ParameterizedTest
+ @MethodSource("schemes")
+ void encryptionIsNotDeterministic(ICryptScheme scheme)
+ {
+ SchemeCrypt crypt = crypt(newKey(), scheme);
+ byte[] plain = "same input".getBytes(StandardCharsets.UTF_8);
+ assertFalse(Arrays.equals(crypt.encrypt(plain), crypt.encrypt(plain)));
+ }
+
+ @ParameterizedTest
+ @MethodSource("schemes")
+ void tamperingAnywhereIsDetected(ICryptScheme scheme)
+ {
+ SchemeCrypt crypt = crypt(newKey(), scheme);
+ byte[] enc = crypt.encrypt("secret payload".getBytes(StandardCharsets.UTF_8));
+
+ // flipping any bit of the scheme payload must be detected (return null)
+ for (int i = 1; i < enc.length; i++)
+ {
+ byte[] tampered = enc.clone();
+ tampered[i] ^= 0x01;
+ assertNull(crypt.decrypt(tampered), "tampering at index " + i + " went undetected");
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("schemes")
+ void wrongKeyCannotDecrypt(ICryptScheme scheme)
+ {
+ byte[] enc = crypt(newKey(), scheme).encrypt("data".getBytes(StandardCharsets.UTF_8));
+ assertNull(crypt(newKey(), scheme).decrypt(enc));
+ }
+
+ @Test
+ void emptyOrShortInputReturnsNull()
+ {
+ SchemeCrypt crypt = crypt(newKey(), new AesGcmCryptScheme());
+ assertNull(crypt.decrypt(new byte[0]));
+ assertNull(crypt.decrypt(new byte[] { AesGcmCryptScheme.ID }));
+ assertNull(crypt.decrypt(new byte[] { AesGcmCryptScheme.ID, 1, 2, 3 }));
+ }
+
+ @Test
+ void nonWhitelistedSchemeIsRefused()
+ {
+ SecretKey key = newKey();
+ byte[] enc = crypt(key, new AesGcmSivCryptScheme())
+ .encrypt("data".getBytes(StandardCharsets.UTF_8));
+
+ AesGcmCryptScheme gcm = new AesGcmCryptScheme();
+ SchemeCrypt gcmOnly = new SchemeCrypt(key, RANDOM, gcm, List.of(gcm));
+ // the SIV marker is not whitelisted -> refuse (downgrade protection)
+ assertNull(gcmOnly.decrypt(enc));
+ }
+
+ @Test
+ void relabelingSchemeMarkerIsRefused()
+ {
+ // both schemes whitelisted; the marker is authenticated, so relabeling ciphertext
+ // from one scheme to another must fail to decrypt
+ SecretKey key = newKey();
+ AesGcmCryptScheme gcm = new AesGcmCryptScheme();
+ AesGcmSivCryptScheme siv = new AesGcmSivCryptScheme();
+ SchemeCrypt crypt = new SchemeCrypt(key, RANDOM, gcm, List.of(gcm, siv));
+
+ byte[] enc = crypt.encrypt("data".getBytes(StandardCharsets.UTF_8));
+ assertEquals(gcm.id(), enc[0]);
+
+ enc[0] = siv.id();
+ assertNull(crypt.decrypt(enc));
+ }
+
+ @Test
+ void migrationBetweenSchemes()
+ {
+ SecretKey key = newKey();
+ AesGcmCryptScheme oldScheme = new AesGcmCryptScheme();
+ AesGcmSivCryptScheme newScheme = new AesGcmSivCryptScheme();
+
+ byte[] legacy = new SchemeCrypt(key, RANDOM, oldScheme, List.of(oldScheme))
+ .encrypt("legacy".getBytes(StandardCharsets.UTF_8));
+
+ // during migration: encrypt with the new scheme, still accept the old one for decryption
+ SchemeCrypt migrating = new SchemeCrypt(key, RANDOM, newScheme,
+ List.of(oldScheme, newScheme));
+ assertArrayEquals("legacy".getBytes(StandardCharsets.UTF_8), migrating.decrypt(legacy));
+
+ byte[] upgraded = migrating.encrypt("fresh".getBytes(StandardCharsets.UTF_8));
+ assertEquals(newScheme.id(), upgraded[0]);
+
+ // after migration: only the new scheme is whitelisted -> old data is refused
+ SchemeCrypt afterMigration = new SchemeCrypt(key, RANDOM, newScheme, List.of(newScheme));
+ assertNull(afterMigration.decrypt(legacy));
+ assertArrayEquals("fresh".getBytes(StandardCharsets.UTF_8),
+ afterMigration.decrypt(upgraded));
+ }
+
+ @Test
+ void urlSafeRoundTrip()
+ {
+ SchemeCrypt crypt = crypt(newKey(), new AesGcmCryptScheme());
+ String value = "a value with / and + and a space";
+ assertEquals(value, crypt.decryptUrlSafe(crypt.encryptUrlSafe(value)));
+ assertNull(crypt.decryptUrlSafe("not-valid-ciphertext"));
+ }
+}
diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/encryption/CryptTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/encryption/CryptTest.java
index 1654edf37a..a47276b2ea 100644
--- a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/encryption/CryptTest.java
+++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/encryption/CryptTest.java
@@ -17,11 +17,9 @@
package org.apache.wicket.markup.html.form.encryption;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import org.apache.wicket.util.crypt.ICrypt;
-import org.apache.wicket.util.crypt.NoCrypt;
-import org.apache.wicket.util.crypt.SunJceCrypt;
+import org.apache.wicket.core.util.crypt.ICrypt;
+import org.apache.wicket.core.util.crypt.NoCrypt;
import org.apache.wicket.util.tester.WicketTestCase;
import org.junit.jupiter.api.Test;
@@ -30,40 +28,12 @@ import org.junit.jupiter.api.Test;
*/
class CryptTest extends WicketTestCase
{
- @Test
- void crypt()
- {
- final SunJceCrypt crypt = new SunJceCrypt(new byte[]{ (byte)0x15, (byte)0x8c, (byte)0xa3, (byte)0x4a,
- (byte)0x66, (byte)0x51, (byte)0x2a, (byte)0xbc }, 17);
- crypt.setKey("someStableKey");
-
- try
- {
- if (crypt.encryptUrlSafe("test") != null)
- {
- final String text = "abcdefghijkABC: A test which creates a '/' and/or a '+'";
- final String expectedUrlSafeEncrypted = "xXMS3UMELV--qVINGVFaYaiqUPOtryc_E4x0MyMFgYl-TgTGKxczTzPvwJrE-4YEVMpl-F3eDAg";
-
- final String encrypted = crypt.encryptUrlSafe(text);
- assertEquals(expectedUrlSafeEncrypted, encrypted);
- assertEquals(text, crypt.decryptUrlSafe(expectedUrlSafeEncrypted));
- assertNull(crypt.decryptUrlSafe("style.css"));
- }
- }
- catch (Exception ex)
- {
- // fails on JVMs without security provider (e.g. seems to be on
- // MAC in US)
- }
- }
-
/**
- *
+ * The {@link NoCrypt} implementation does not modify the string at all.
*/
@Test
void noCrypt()
{
- // The NoCrypt implementation does not modify the string at all
final ICrypt crypt = new NoCrypt();
assertEquals("test", crypt.encryptUrlSafe("test"));
diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java
index b1224d99b8..5c6b763903 100644
--- a/wicket-core-tests/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java
+++ b/wicket-core-tests/src/test/java/org/apache/wicket/pageStore/CryptingPageStoreTest.java
@@ -17,19 +17,17 @@
package org.apache.wicket.pageStore;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertNull;
-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.core.util.crypt.AesGcmCryptScheme;
+import org.apache.wicket.core.util.crypt.AesGcmSivCryptScheme;
+import org.apache.wicket.core.util.crypt.ICryptScheme;
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.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -39,7 +37,7 @@ import org.junit.jupiter.params.provider.MethodSource;
/**
* Test for {@link CryptingPageStore}.
- *
+ *
* @author svenmeier
*/
public class CryptingPageStoreTest extends WicketTestCase
@@ -49,17 +47,17 @@ public class CryptingPageStoreTest extends WicketTestCase
{
Security.addProvider(new BouncyCastleProvider());
}
-
- static List<ICrypter> crypters()
+
+ static List<ICryptScheme> schemes()
{
- return List.of(new DefaultCrypter(), new GCMSIVCrypter());
+ return List.of(new AesGcmCryptScheme(), new AesGcmSivCryptScheme());
}
@ParameterizedTest
- @MethodSource("crypters")
- void test(ICrypter crypter)
+ @MethodSource("schemes")
+ void test(ICryptScheme scheme)
{
- CryptingPageStore store = buildPageStore(crypter);
+ CryptingPageStore store = buildPageStore(scheme, new MockPageStore());
JavaSerializer serializer = new JavaSerializer("test");
IPageContext context = new MockPageContext();
@@ -77,10 +75,10 @@ public class CryptingPageStoreTest extends WicketTestCase
}
@ParameterizedTest
- @MethodSource("crypters")
- void testFail(ICrypter crypter)
+ @MethodSource("schemes")
+ void wrongKeyCannotDecrypt(ICryptScheme scheme)
{
- CryptingPageStore store = buildPageStore(crypter);
+ CryptingPageStore store = buildPageStore(scheme, new MockPageStore());
JavaSerializer serializer = new JavaSerializer("test");
MockPageContext context = new MockPageContext();
@@ -91,34 +89,42 @@ public class CryptingPageStoreTest extends WicketTestCase
SerializedPage serializedAdd = new SerializedPage(p, "foo", serializer.serialize(add));
store.addPage(context, serializedAdd);
- // remove key from session
+ // remove key from session, so a new key will be generated on the next access
context.clearSession();
- try
- {
- SerializedPage serializedGot = (SerializedPage) store.getPage(context, p);
+ // the page can no longer be decrypted and is therefore treated as a cache miss
+ assertNull(store.getPage(context, p));
+ }
- MockPage got = (MockPage) serializer.deserialize(serializedGot.getData());
- assertEquals(p, got.getPageId());
- }
- catch (RuntimeException ex)
- {
- assertTrue(
- ex.getCause() instanceof GeneralSecurityException
- || ex.getCause() instanceof StreamCorruptedException,
- "unable to decrypt with new key");
- }
+ @ParameterizedTest
+ @MethodSource("schemes")
+ void tamperedDataCannotDecrypt(ICryptScheme scheme)
+ {
+ MockPageStore delegate = new MockPageStore();
+ CryptingPageStore store = buildPageStore(scheme, delegate);
+ JavaSerializer serializer = new JavaSerializer("test");
+
+ MockPageContext context = new MockPageContext();
+
+ int p = 7;
+ store.addPage(context, new SerializedPage(p, "foo", serializer.serialize(new MockPage(p))));
+
+ // tamper with the stored ciphertext
+ SerializedPage stored = (SerializedPage) delegate.getPage(context, p);
+ byte[] data = stored.getData();
+ data[data.length - 1] ^= 0x01;
+ delegate.addPage(context, new SerializedPage(p, "foo", data));
+
+ // authenticated encryption must detect the tampering; the page is treated as a cache miss
+ assertNull(store.getPage(context, p));
}
-
- private CryptingPageStore buildPageStore(ICrypter crypter)
+
+ private CryptingPageStore buildPageStore(ICryptScheme scheme, MockPageStore delegate)
{
- return new CryptingPageStore(new MockPageStore(), tester.getApplication())
- {
- @Override
- protected ICrypter newCrypter()
- {
- return crypter;
- }
- };
+ tester.getApplication()
+ .getSecuritySettings()
+ .setCryptScheme(scheme)
+ .setWhitelistedCryptSchemes(List.of(scheme));
+ return new CryptingPageStore(delegate, tester.getApplication());
}
}
diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/protocol/http/request/CryptedUrlWebRequestCodingStrategyTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/protocol/http/request/CryptedUrlWebRequestCodingStrategyTest.java
index 64a066034d..0c60aeeb11 100644
--- a/wicket-core-tests/src/test/java/org/apache/wicket/protocol/http/request/CryptedUrlWebRequestCodingStrategyTest.java
+++ b/wicket-core-tests/src/test/java/org/apache/wicket/protocol/http/request/CryptedUrlWebRequestCodingStrategyTest.java
@@ -18,16 +18,14 @@ package org.apache.wicket.protocol.http.request;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import java.util.Base64;
-
import org.apache.wicket.core.request.mapper.CryptoMapper;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.markup.html.basic.SimplePage;
import org.apache.wicket.mock.MockApplication;
import org.apache.wicket.protocol.http.WebApplication;
+import org.apache.wicket.core.util.crypt.ICrypt;
+import org.apache.wicket.core.util.crypt.ICryptFactory;
import org.apache.wicket.request.mapper.CompoundRequestMapper;
-import org.apache.wicket.util.crypt.ICrypt;
-import org.apache.wicket.util.crypt.ICryptFactory;
import org.apache.wicket.util.tester.WicketTestCase;
import org.junit.jupiter.api.Test;
@@ -107,21 +105,20 @@ class CryptedUrlWebRequestCodingStrategyTest extends WicketTestCase
@Override
public ICrypt newCrypt()
{
+ // identity byte[] crypt: the default url-safe methods then simply Base64-encode the URL
return new ICrypt()
{
-
@Override
- public String decryptUrlSafe(String text)
+ public byte[] encrypt(byte[] plainBytes)
{
- return new String(Base64.getUrlDecoder().decode(text));
+ return plainBytes;
}
@Override
- public String encryptUrlSafe(String plainText)
+ public byte[] decrypt(byte[] encryptedBytes)
{
- return new String(Base64.getUrlEncoder().encode(plainText.getBytes()));
+ return encryptedBytes;
}
-
};
}
}
diff --git a/wicket-core/pom.xml b/wicket-core/pom.xml
index 04a1fd707c..c384f85a84 100644
--- a/wicket-core/pom.xml
+++ b/wicket-core/pom.xml
@@ -114,7 +114,6 @@ org.apache.wicket.model;-noimport:=true,
org.apache.wicket.model.util;-noimport:=true,
org.apache.wicket.page;-noimport:=true,
org.apache.wicket.pageStore;-noimport:=true,
-org.apache.wicket.pageStore.crypt;-noimport:=true,
org.apache.wicket.pageStore.disk;-noimport:=true,
org.apache.wicket.protocol.http;-noimport:=true,
org.apache.wicket.protocol.http.mock;-noimport:=true,
diff --git a/wicket-core/src/main/java/module-info.java b/wicket-core/src/main/java/module-info.java
index e7bd7b3a4b..cb87eb0452 100644
--- a/wicket-core/src/main/java/module-info.java
+++ b/wicket-core/src/main/java/module-info.java
@@ -111,7 +111,6 @@ module org.apache.wicket.core {
exports org.apache.wicket.model.util;
exports org.apache.wicket.page;
exports org.apache.wicket.pageStore;
- exports org.apache.wicket.pageStore.crypt;
exports org.apache.wicket.pageStore.disk;
exports org.apache.wicket.protocol.http;
exports org.apache.wicket.protocol.http.mock;
diff --git a/wicket-core/src/main/java/org/apache/wicket/authentication/strategy/DefaultAuthenticationStrategy.java b/wicket-core/src/main/java/org/apache/wicket/authentication/strategy/DefaultAuthenticationStrategy.java
index b905858f64..7aef039229 100644
--- a/wicket-core/src/main/java/org/apache/wicket/authentication/strategy/DefaultAuthenticationStrategy.java
+++ b/wicket-core/src/main/java/org/apache/wicket/authentication/strategy/DefaultAuthenticationStrategy.java
@@ -17,9 +17,9 @@
package org.apache.wicket.authentication.strategy;
import org.apache.wicket.authentication.IAuthenticationStrategy;
+import org.apache.wicket.core.util.crypt.ICrypt;
import org.apache.wicket.util.cookies.CookieDefaults;
import org.apache.wicket.util.cookies.CookieUtils;
-import org.apache.wicket.util.crypt.ICrypt;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
@@ -95,17 +95,13 @@ public class DefaultAuthenticationStrategy implements IAuthenticationStrategy
String value = getCookieUtils().load(cookieKey);
if (Strings.isEmpty(value) == false)
{
- try
- {
- value = getCrypt().decryptUrlSafe(value);
- }
- catch (RuntimeException e)
+ value = getCrypt().decryptUrlSafe(value);
+ if (value == null)
{
logger.info(
- "Error decrypting login cookie: {}. The cookie will be deleted. Possible cause is that a session-relative encryption key was used to encrypt this cookie while this decryption attempt is happening in a different session, eg user coming back to the application after session expiration",
+ "Could not decrypt login cookie: {}. The cookie will be deleted. A possible cause is that the encryption key changed (e.g. the application was restarted with a session-relative or per-boot key), so a cookie created earlier can no longer be decrypted",
cookieKey);
getCookieUtils().remove(cookieKey);
- value = null;
}
return decode(value);
}
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/CryptoMapper.java b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/CryptoMapper.java
index 333250ecbf..d222666ce6 100755
--- a/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/CryptoMapper.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/request/mapper/CryptoMapper.java
@@ -16,21 +16,24 @@
*/
package org.apache.wicket.core.request.mapper;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.function.Supplier;
import org.apache.wicket.Application;
+import org.apache.wicket.MetaDataKey;
import org.apache.wicket.core.request.handler.RequestSettingRequestHandler;
+import org.apache.wicket.core.util.crypt.ICrypt;
import org.apache.wicket.protocol.http.PageExpiredException;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.IRequestMapper;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Url;
+import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.mapper.IRequestMapperDelegate;
import org.apache.wicket.request.mapper.info.PageComponentInfo;
-import org.apache.wicket.util.crypt.ICrypt;
-import org.apache.wicket.util.crypt.ICryptFactory;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
@@ -46,8 +49,8 @@ import org.slf4j.LoggerFactory;
* <p>
* <strong>Important</strong>: for better security it is recommended to use
* {@link org.apache.wicket.core.request.mapper.CryptoMapper#CryptoMapper(IRequestMapper, Supplier)}
- * constructor with {@link org.apache.wicket.util.crypt.ICrypt} implementation that generates a
- * separate key for each user. {@link org.apache.wicket.core.util.crypt.KeyInSessionSunJceCryptFactory} provides such an
+ * constructor with {@link org.apache.wicket.core.util.crypt.ICrypt} implementation that generates a
+ * separate key for each user. {@link org.apache.wicket.core.util.crypt.KeyInSessionCryptFactory} provides such an
* implementation that stores the key in the HTTP session.
* </p>
*
@@ -79,16 +82,15 @@ import org.slf4j.LoggerFactory;
*
* <p>
* {@link CryptoMapper} can be configured to mark encrypted URLs as encrypted, and throw a {@link PageExpiredException}
- * exception if a encrypted URL cannot be decrypted. This can occur when using {@code KeyInSessionSunJceCryptFactory}, and
+ * exception if a encrypted URL cannot be decrypted. This can occur when using {@code KeyInSessionCryptFactory}, and
* the session has expired.
* </p>
- *
+ *
* @author igor.vaynberg
* @author Jesse Long
* @author svenmeier
- * @see org.apache.wicket.settings.SecuritySettings#setCryptFactory(org.apache.wicket.util.crypt.ICryptFactory)
- * @see org.apache.wicket.core.util.crypt.KeyInSessionSunJceCryptFactory
- * @see org.apache.wicket.util.crypt.SunJceCrypt
+ * @see org.apache.wicket.settings.SecuritySettings#setCryptFactory(org.apache.wicket.core.util.crypt.ICryptFactory)
+ * @see org.apache.wicket.core.util.crypt.KeyInSessionCryptFactory
*/
public class CryptoMapper implements IRequestMapperDelegate
{
@@ -101,6 +103,20 @@ public class CryptoMapper implements IRequestMapperDelegate
private static final String ENCRYPTED_URL_MARKER_PREFIX = "crypt.";
+ /**
+ * Per-request cache of {@code plaintext -> ciphertext} learned while decrypting the incoming
+ * URL. Authenticated encryption uses a random nonce, so re-encrypting the same plaintext
+ * would otherwise yield a different ciphertext than the one requested, and Wicket's URL
+ * normalisation would redirect endlessly. By reusing the ciphertext we received for a given
+ * plaintext, the URL generated for the current page matches the requested one. A nonce is
+ * thereby only ever reused for the identical plaintext (reproducing the same ciphertext),
+ * never across different plaintexts.
+ */
+ private static final MetaDataKey<HashMap<String, String>> ENCRYPTED_URL_CACHE = new MetaDataKey<>()
+ {
+ private static final long serialVersionUID = 1L;
+ };
+
private final IRequestMapper wrappedMapper;
private final Supplier<ICrypt> cryptProvider;
@@ -115,7 +131,7 @@ public class CryptoMapper implements IRequestMapperDelegate
* <strong>Important</strong>: For better security it is recommended to use
* {@link CryptoMapper#CryptoMapper(IRequestMapper, Supplier)} with a specific {@link ICrypt} implementation
* that generates a separate key for each user.
- * {@link org.apache.wicket.core.util.crypt.KeyInSessionSunJceCryptFactory} provides such an implementation that stores the
+ * {@link org.apache.wicket.core.util.crypt.KeyInSessionCryptFactory} provides such an implementation that stores the
* key in the HTTP session.
* </p>
*
@@ -123,7 +139,6 @@ public class CryptoMapper implements IRequestMapperDelegate
* the non-crypted request mapper
* @param application
* the current application
- * @see org.apache.wicket.util.crypt.SunJceCrypt
*/
public CryptoMapper(final IRequestMapper wrappedMapper, final Application application)
{
@@ -245,6 +260,71 @@ public class CryptoMapper implements IRequestMapperDelegate
return cryptProvider.get();
}
+ /**
+ * Encrypts the given plain text. If the current request already carried the same plain text in
+ * encrypted form, that exact ciphertext is reused so the generated URL matches the requested
+ * one (see {@link #ENCRYPTED_URL_CACHE}); otherwise a fresh (random-nonce) ciphertext is
+ * produced and remembered for the remainder of the request.
+ *
+ * @param plainText
+ * the text to encrypt
+ * @return the encrypted, URL-safe text
+ */
+ private String encryptString(final String plainText)
+ {
+ Map<String, String> memo = getEncryptionMemo();
+ if (memo != null)
+ {
+ String cached = memo.get(plainText);
+ // only reuse ciphertext that our own crypt decrypts back to the same plain text
+ // (guards against a shared request when multiple CryptoMappers are installed)
+ if (cached != null && plainText.equals(getCrypt().decryptUrlSafe(cached)))
+ {
+ return cached;
+ }
+ }
+
+ String encrypted = getCrypt().encryptUrlSafe(plainText);
+ if (memo != null)
+ {
+ memo.putIfAbsent(plainText, encrypted);
+ }
+ return encrypted;
+ }
+
+ /**
+ * Remembers, for the remainder of the current request, that {@code plainText} was received as
+ * {@code cipherText}, so that re-encrypting it reproduces the same ciphertext.
+ */
+ private void rememberEncryption(final String plainText, final String cipherText)
+ {
+ Map<String, String> memo = getEncryptionMemo();
+ if (memo != null && plainText != null && cipherText != null)
+ {
+ memo.putIfAbsent(plainText, cipherText);
+ }
+ }
+
+ /**
+ * @return the per-request {@code plaintext -> ciphertext} cache, or {@code null} if there is no
+ * current request cycle (e.g. when generating URLs outside a request)
+ */
+ private Map<String, String> getEncryptionMemo()
+ {
+ RequestCycle cycle = RequestCycle.get();
+ if (cycle == null)
+ {
+ return null;
+ }
+ HashMap<String, String> memo = cycle.getMetaData(ENCRYPTED_URL_CACHE);
+ if (memo == null)
+ {
+ memo = new HashMap<>();
+ cycle.setMetaData(ENCRYPTED_URL_CACHE, memo);
+ }
+ return memo;
+ }
+
/**
* @return the wrapped root request mapper
*/
@@ -296,7 +376,7 @@ public class CryptoMapper implements IRequestMapperDelegate
*/
protected Url encryptEntireUrl(final Url url)
{
- String encryptedUrlString = getCrypt().encryptUrlSafe(url.toString());
+ String encryptedUrlString = encryptString(url.toString());
Url encryptedUrl = new Url(url.getCharset());
@@ -338,7 +418,7 @@ public class CryptoMapper implements IRequestMapperDelegate
if (MapperUtils.parsePageComponentInfoParameter(qp) != null)
{
it.remove();
- String encryptedParameterValue = getCrypt().encryptUrlSafe(qp.getName());
+ String encryptedParameterValue = encryptString(qp.getName());
Url.QueryParameter encryptedParameter
= new Url.QueryParameter(ENCRYPTED_PAGE_COMPONENT_INFO_PARAMETER, encryptedParameterValue);
encryptedUrl.getQueryParameters().add(0, encryptedParameter);
@@ -470,6 +550,9 @@ public class CryptoMapper implements IRequestMapperDelegate
}
}
+ // remember the ciphertext we received, so re-encrypting this url reproduces it
+ rememberEncryption(decryptedUrl, encryptedUrlString);
+
Url originalUrl = Url.parse(decryptedUrl, request.getCharset());
int originalNumberOfSegments = originalUrl.getSegments().size();
@@ -601,6 +684,8 @@ public class CryptoMapper implements IRequestMapperDelegate
}
else
{
+ // remember the ciphertext we received, so re-encrypting reproduces it
+ rememberEncryption(decryptedValue, encryptedValue);
Url.QueryParameter decryptedParamter = new Url.QueryParameter(decryptedValue, "");
url.getQueryParameters().add(0, decryptedParamter);
}
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AESCrypt.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AESCrypt.java
deleted file mode 100644
index 59d1d4893e..0000000000
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AESCrypt.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * 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.core.util.crypt;
-
-import org.apache.wicket.core.random.ISecureRandomSupplier;
-import org.apache.wicket.util.crypt.ICrypt;
-import org.apache.wicket.util.lang.Args;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-
-/**
- * AES based {@link ICrypt} encrypt and decrypt strings such as passwords or URL segments.
- * Based on http://stackoverflow.com/a/992413
- *
- * @see ICrypt
- */
-public class AESCrypt extends AbstractJceCrypt
-{
-
- private final SecretKey secretKey;
- private final String algorithm;
- private final ISecureRandomSupplier randomSupplier;
-
-
- /**
- * Constructor
- *
- * @param secretKey
- * The {@link SecretKey} to use to initialize the {@link Cipher}.
- * @param randomSupplier
- * The {@link ISecureRandomSupplier} to use to generate random values.
- */
- public AESCrypt(SecretKey secretKey, ISecureRandomSupplier randomSupplier)
- {
- this(secretKey, "AES/CBC/PKCS5Padding", randomSupplier);
- }
-
- /**
- * Constructor
- *
- * @param secretKey
- * The {@link SecretKey} to use to initialize the {@link Cipher}.
- * @param algorithm
- * The cipher algorithm to use, for example "AES/CBC/PKCS5Padding".
- * @param randomSupplier
- * The {@link ISecureRandomSupplier} to use to generate random values.
- */
- public AESCrypt(SecretKey secretKey, String algorithm,
- ISecureRandomSupplier randomSupplier)
- {
- Args.notNull(secretKey, "secretKey");
- Args.notNull(algorithm, "algorithm");
- Args.notNull(randomSupplier, "randomSupplier");
-
- this.secretKey = secretKey;
- this.algorithm = algorithm;
- this.randomSupplier = randomSupplier;
- }
-
- @Override
- protected byte[] decrypt(byte[] encrypted)
- {
- try
- {
- Cipher cipher = Cipher.getInstance(algorithm);
-
- int ivSize = cipher.getBlockSize();
- byte[] iv = new byte[ivSize];
- byte[] ciphertext = new byte[encrypted.length - ivSize];
-
- System.arraycopy(encrypted, 0, iv, 0, ivSize);
- System.arraycopy(encrypted, ivSize, ciphertext, 0, ciphertext.length);
-
- cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
- return cipher.doFinal(ciphertext);
- }
- catch (IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException
- | InvalidKeyException | InvalidAlgorithmParameterException e)
- {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- protected byte[] encrypt(byte[] plainBytes)
- {
- try
- {
- Cipher cipher = Cipher.getInstance(algorithm);
- int ivSize = cipher.getBlockSize();
- byte[] iv = randomSupplier.getRandomBytes(ivSize);
- cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
-
- byte[] ciphertext = cipher.doFinal(plainBytes);
- byte[] finalRes = new byte[ciphertext.length + ivSize];
-
- System.arraycopy(iv, 0, finalRes, 0, ivSize);
- System.arraycopy(ciphertext, 0, finalRes, ivSize, ciphertext.length);
-
- return finalRes;
- }
- catch (IllegalBlockSizeException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException
- | InvalidKeyException | InvalidAlgorithmParameterException e)
- {
- throw new RuntimeException(e);
- }
-
- }
-}
\ No newline at end of file
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
new file mode 100644
index 0000000000..ed12284dde
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractCryptFactory.java
@@ -0,0 +1,59 @@
+/*
+ * 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.core.util.crypt;
+
+import java.security.SecureRandom;
+
+import javax.crypto.SecretKey;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.settings.SecuritySettings;
+
+/**
+ * Base {@link ICryptFactory} that builds a {@link SchemeCrypt} from a key supplied by the concrete
+ * factory and the encryption scheme + decryption whitelist configured on the application's
+ * {@link SecuritySettings}. Subclasses only provide the key source (see {@link #getKey()}).
+ */
+public abstract class AbstractCryptFactory implements ICryptFactory
+{
+ @Override
+ public ICrypt newCrypt()
+ {
+ return createCrypt(getKey());
+ }
+
+ /**
+ * Builds an {@link ICrypt} for the given key using the application's crypto policy.
+ *
+ * @param key
+ * the secret key to bind
+ * @return a new {@link SchemeCrypt}
+ */
+ protected ICrypt createCrypt(SecretKey key)
+ {
+ SecuritySettings settings = Application.get().getSecuritySettings();
+ SecureRandom random = settings.getRandomSupplier().getRandom();
+ return new SchemeCrypt(key, random, settings.getCryptScheme(),
+ settings.getWhitelistedCryptSchemes());
+ }
+
+ /**
+ * @return the secret key to use; the key source (per-session, global, ...) is the concrete
+ * factory's responsibility
+ */
+ protected abstract SecretKey getKey();
+}
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractJceCrypt.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractJceCrypt.java
deleted file mode 100644
index 4f5d7b6020..0000000000
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractJceCrypt.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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.core.util.crypt;
-
-import org.apache.wicket.util.crypt.ICrypt;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.util.Base64;
-
-
-/**
- * Base class for JCE based ICrypt implementations.
- *
- */
-public abstract class AbstractJceCrypt implements ICrypt
-{
- /** Log. */
- private static final Logger log = LoggerFactory.getLogger(AbstractJceCrypt.class);
-
- /**
- * Decrypts a string into a string.
- *
- * @param text
- * text to decrypt
- * @return the decrypted text
- */
- @Override
- public final String decryptUrlSafe(final String text)
- {
- try
- {
- byte[] decoded = java.util.Base64.getUrlDecoder().decode(text);
- return new String(decrypt(decoded), StandardCharsets.UTF_8);
- }
- catch (Exception ex)
- {
- log.debug("Error decoding text: {}", text, ex);
- return null;
- }
- }
-
- /**
- * Encrypt a string into a string using URL safe Base64 encoding.
- *
- * @param plainText
- * text to encrypt
- * @return encrypted string
- */
- @Override
- public final String encryptUrlSafe(final String plainText)
- {
- byte[] encrypted = encrypt(plainText.getBytes(StandardCharsets.UTF_8));
- Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
- byte[] encoded = encoder.encode(encrypted);
- return new String(encoded, StandardCharsets.UTF_8);
- }
-
- /**
- * Decrypts an encrypted byte array.
- *
- * @param encrypted
- * byte array to decrypt
- * @return the decrypted text
- */
- abstract protected byte[] decrypt(final byte[] encrypted);
-
-
- /**
- * Encrypts the given text into a byte array.
- *
- * @param plainBytes
- * text to encrypt
- * @return the string encrypted
- */
- abstract protected byte[] encrypt(final byte[] plainBytes);
-}
\ No newline at end of file
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractKeyInSessionCryptFactory.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractKeyInSessionCryptFactory.java
deleted file mode 100644
index f1cc2b2ae0..0000000000
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractKeyInSessionCryptFactory.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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.core.util.crypt;
-
-import java.io.Serializable;
-
-import org.apache.wicket.MetaDataKey;
-import org.apache.wicket.Session;
-import org.apache.wicket.util.crypt.ICrypt;
-import org.apache.wicket.util.crypt.ICryptFactory;
-import org.apache.wicket.util.io.IClusterable;
-
-
-/**
- * Base class to implement crypt factories that store crypt into user session. Note that the use of
- * this crypt factory will result in an immediate creation of a http session.
- *
- * @author andrea del bene
- *
- * @param <T>
- * the type for the secret key.
- */
-public abstract class AbstractKeyInSessionCryptFactory<T extends IClusterable>
- implements
- ICryptFactory
-{
- /** metadata-key used to store crypto-key in session metadata */
- private static final MetaDataKey<Serializable> KEY = new MetaDataKey<>()
- {
- private static final long serialVersionUID = 1L;
- };
-
- /**
- * Creates a new crypt for the current user session. If no user session is available, a new one
- * is created.
- *
- * @return
- */
- @Override
- public ICrypt newCrypt()
- {
- Session session = Session.get();
- session.bind();
-
- // retrieve or generate encryption key from session
- T key = (T) session.getMetaData(KEY);
- if (key == null)
- {
- // generate new key
- key = generateKey(session);
- session.setMetaData(KEY, key);
- }
-
- // build the crypt based on session key
- ICrypt crypt = createCrypt(key);
- return crypt;
- }
-
- /**
- * Generates the secret key for a new crypt.
- *
- * @param session
- * the current user session where crypt will be stored
- * @return the secret key for a new crypt
- */
- protected abstract T generateKey(Session session);
-
- /**
- * @return the {@link org.apache.wicket.util.crypt.ICrypt} to use
- */
- protected abstract ICrypt createCrypt(T key);
-}
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
new file mode 100644
index 0000000000..1ce04fb914
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmCryptScheme.java
@@ -0,0 +1,121 @@
+/*
+ * 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.core.util.crypt;
+
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+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>
+ * 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 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
+{
+ /** 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()
+ {
+ return ID;
+ }
+
+ @Override
+ public byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, SecureRandom random)
+ {
+ 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);
+ }
+ }
+
+ @Override
+ public byte[] decrypt(byte[] ciphertext, SecretKey key, byte[] aad)
+ {
+ 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;
+ }
+ }
+}
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
new file mode 100644
index 0000000000..d96cd90820
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AesGcmSivCryptScheme.java
@@ -0,0 +1,121 @@
+/*
+ * 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.core.util.crypt;
+
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+
+import org.apache.wicket.WicketRuntimeException;
+import org.bouncycastle.jcajce.spec.AEADParameterSpec;
+
+/**
+ * Authenticated encryption using AES-256-GCM-SIV ({@code AES/GCM-SIV/NoPadding}).
+ * <p>
+ * This scheme requires <a href="https://www.bouncycastle.org/">Bouncy Castle</a> (an optional
+ * Wicket dependency) with its provider registered. Compared to {@link AesGcmCryptScheme} it adds
+ * nonce-misuse resistance (safe even on accidental nonce reuse or beyond the random-nonce
+ * usage bound), at the cost of the extra dependency and a slower software implementation until
+ * the JDK ships native GCM-SIV (<a href="https://bugs.openjdk.org/browse/JDK-8256530">JDK-8256530</a>).
+ * <p>
+ * 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
+{
+ /** 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()
+ {
+ return ID;
+ }
+
+ @Override
+ public byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, SecureRandom random)
+ {
+ 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);
+ }
+ }
+
+ @Override
+ public byte[] decrypt(byte[] ciphertext, SecretKey key, byte[] aad)
+ {
+ 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;
+ }
+ }
+}
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
new file mode 100644
index 0000000000..16415a458b
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ApplicationKeyCryptFactory.java
@@ -0,0 +1,66 @@
+/*
+ * 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.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;
+
+/**
+ * An {@link ICryptFactory} using a single, application-wide key. Unlike
+ * {@link KeyInSessionCryptFactory} the key is not tied to a session, so data encrypted with it can
+ * be decrypted across sessions — required for things like a "remember me" cookie.
+ * <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).
+ */
+public class ApplicationKeyCryptFactory extends AbstractCryptFactory
+{
+ private final SecretKey key;
+
+ /**
+ * @param key
+ * the application-wide key to use
+ */
+ public ApplicationKeyCryptFactory(SecretKey key)
+ {
+ this.key = Args.notNull(key, "key");
+ }
+
+ /**
+ * Generates a random application-wide key. 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"));
+ }
+
+ @Override
+ protected SecretKey getKey()
+ {
+ return key;
+ }
+}
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICrypt.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICrypt.java
new file mode 100644
index 0000000000..76852ddb87
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICrypt.java
@@ -0,0 +1,95 @@
+/*
+ * 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.core.util.crypt;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * The unified encryption/decryption abstraction used throughout Wicket (page store, URL
+ * encryption and authentication cookies).
+ * <p>
+ * Implementations operate on raw {@code byte[]} and are expected to produce
+ * <em>self-describing</em>, authenticated ciphertext (see {@link ICryptScheme} and
+ * {@link SchemeCrypt}). The {@link #decrypt(byte[])} contract is to return {@code null} on
+ * <em>any</em> failure (unknown/non-whitelisted scheme, authentication failure, malformed
+ * input) rather than throwing, so callers can uniformly treat undecryptable data as
+ * absent/expired.
+ * <p>
+ * The default {@link #encryptUrlSafe(String)} / {@link #decryptUrlSafe(String)} methods add a
+ * URL-safe Base64 {@code String} layer on top of the {@code byte[]} core.
+ */
+public interface ICrypt
+{
+ /**
+ * Encrypt the given bytes.
+ *
+ * @param plainBytes
+ * the bytes to encrypt, must not be {@code null}
+ * @return the encrypted bytes
+ */
+ byte[] encrypt(byte[] plainBytes);
+
+ /**
+ * Decrypt the given bytes.
+ *
+ * @param encryptedBytes
+ * the bytes to decrypt
+ * @return the decrypted bytes, or {@code null} if the input could not be decrypted for any
+ * reason (unknown or non-whitelisted scheme, failed authentication, malformed input)
+ */
+ byte[] decrypt(byte[] encryptedBytes);
+
+ /**
+ * Encrypt the given text into a URL-safe (Base64, no padding) {@code String}.
+ *
+ * @param plainText
+ * the text to encrypt
+ * @return the encrypted, URL-safe text
+ */
+ default String encryptUrlSafe(String plainText)
+ {
+ byte[] encrypted = encrypt(plainText.getBytes(StandardCharsets.UTF_8));
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(encrypted);
+ }
+
+ /**
+ * Decrypt a URL-safe (Base64) {@code String} produced by {@link #encryptUrlSafe(String)}.
+ *
+ * @param encryptedText
+ * the encrypted, URL-safe text
+ * @return the decrypted text, or {@code null} if it could not be decrypted
+ */
+ default String decryptUrlSafe(String encryptedText)
+ {
+ try
+ {
+ byte[] decoded = Base64.getUrlDecoder().decode(encryptedText);
+ byte[] decrypted = decrypt(decoded);
+ if (decrypted == null)
+ {
+ return null;
+ }
+ return new String(decrypted, StandardCharsets.UTF_8);
+ }
+ catch (RuntimeException ex)
+ {
+ // e.g. invalid Base64 input
+ return null;
+ }
+ }
+}
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/crypt/ICryptFactory.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICryptFactory.java
similarity index 67%
rename from wicket-util/src/main/java/org/apache/wicket/util/crypt/ICryptFactory.java
rename to wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICryptFactory.java
index d48cdb8097..450531f6bd 100644
--- a/wicket-util/src/main/java/org/apache/wicket/util/crypt/ICryptFactory.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICryptFactory.java
@@ -14,18 +14,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.wicket.util.crypt;
-
+package org.apache.wicket.core.util.crypt;
/**
- * Interface used to generate crypt objects
- *
- * @author Igor Vaynberg (ivaynberg)
+ * A factory producing {@link ICrypt} instances. The factory owns the <em>key source</em> (e.g. a
+ * per-session key or a global application key); the encryption scheme and decryption whitelist
+ * come from the application's
+ * {@link org.apache.wicket.settings.SecuritySettings crypto settings}.
*/
public interface ICryptFactory
{
/**
- * @return new crypt object
+ * @return a (possibly cached) {@link ICrypt} bound to this factory's key
*/
ICrypt newCrypt();
}
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
new file mode 100644
index 0000000000..9eeff286a2
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/ICryptScheme.java
@@ -0,0 +1,74 @@
+/*
+ * 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.core.util.crypt;
+
+import java.security.SecureRandom;
+
+import javax.crypto.SecretKey;
+
+/**
+ * A single, self-identifying authenticated encryption scheme.
+ * <p>
+ * Every scheme has a stable {@link #id()} that {@link SchemeCrypt} writes as a one-byte marker
+ * in front of the ciphertext, so the correct scheme can be selected on decryption and matched
+ * against a whitelist (downgrade protection). The marker is passed to {@link #encrypt} /
+ * {@link #decrypt} as <em>additional authenticated data</em> ({@code aad}) so it is covered by
+ * 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.
+ * <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}.
+ */
+public interface ICryptScheme
+{
+ /**
+ * @return the stable, unique one-byte marker identifying this scheme
+ */
+ byte id();
+
+ /**
+ * Encrypt the given plaintext.
+ *
+ * @param plaintext
+ * the bytes to encrypt
+ * @param key
+ * the secret key
+ * @param aad
+ * additional authenticated data (the scheme marker); authenticated but not encrypted
+ * @param random
+ * source of randomness for the nonce
+ * @return the ciphertext (including nonce and authentication tag)
+ */
+ byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, SecureRandom random);
+
+ /**
+ * Decrypt the given ciphertext.
+ *
+ * @param ciphertext
+ * the bytes produced by {@link #encrypt} (nonce + ciphertext + tag)
+ * @param key
+ * the secret key
+ * @param aad
+ * the additional authenticated data that was supplied on encryption (the marker)
+ * @return the decrypted plaintext, or {@code null} if authentication fails or the input is
+ * malformed
+ */
+ byte[] decrypt(byte[] ciphertext, SecretKey key, byte[] aad);
+}
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
new file mode 100644
index 0000000000..2b9eea54ab
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionCryptFactory.java
@@ -0,0 +1,74 @@
+/*
+ * 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.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
+ * stores it in the session's metadata. Using this factory forces the immediate creation of an
+ * HTTP session.
+ * <p>
+ * Because the key is per session, data encrypted for one session cannot be decrypted from
+ * another (or after the session has expired) — which is exactly what makes it safe to
+ * encrypt session-scoped data such as URLs.
+ */
+public class KeyInSessionCryptFactory extends AbstractCryptFactory
+{
+ /** metadata-key used to store the crypto key in session metadata */
+ private static final MetaDataKey<SecretKey> KEY = new MetaDataKey<>()
+ {
+ private static final long serialVersionUID = 1L;
+ };
+
+ @Override
+ protected SecretKey getKey()
+ {
+ Session session = Session.get();
+ session.bind();
+
+ SecretKey key = session.getMetaData(KEY);
+ if (key == null)
+ {
+ key = generateKey(
+ Application.get().getSecuritySettings().getRandomSupplier().getRandom());
+ session.setMetaData(KEY, key);
+ }
+ 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/core/util/crypt/KeyInSessionSunJceCryptFactory.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionSunJceCryptFactory.java
deleted file mode 100644
index 49f69d0d4c..0000000000
--- a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/KeyInSessionSunJceCryptFactory.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * 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.core.util.crypt;
-
-import org.apache.wicket.Session;
-import org.apache.wicket.core.util.crypt.KeyInSessionSunJceCryptFactory.CryptData;
-import org.apache.wicket.util.crypt.ICrypt;
-import org.apache.wicket.util.crypt.SunJceCrypt;
-import org.apache.wicket.util.io.IClusterable;
-import org.apache.wicket.util.lang.Args;
-
-import java.security.Provider;
-import java.security.Security;
-import java.util.UUID;
-
-/**
- * Crypt factory that produces {@link SunJceCrypt} instances based on session-specific
- * encryption key. This allows each user to have his own encryption key, hardening against CSRF
- * attacks.
- * <br>
- * Note that the use of this crypt factory will result in an immediate creation of a session.
- *
- * @author igor.vaynberg
- */
-public class KeyInSessionSunJceCryptFactory extends AbstractKeyInSessionCryptFactory<CryptData>
-{
-
- private final String cryptMethod;
-
- /**
- * Constructor using {@link javax.crypto.Cipher} {@value org.apache.wicket.util.crypt.SunJceCrypt#DEFAULT_CRYPT_METHOD}
- */
- public KeyInSessionSunJceCryptFactory()
- {
- this(SunJceCrypt.DEFAULT_CRYPT_METHOD);
- }
-
- /**
- * Constructor that uses a custom {@link javax.crypto.Cipher}
- *
- * @param cryptMethod
- * the name of the crypt method (cipher)
- */
- public KeyInSessionSunJceCryptFactory(String cryptMethod)
- {
- this.cryptMethod = Args.notNull(cryptMethod, "Crypt method");
-
- final Provider[] providers = Security.getProviders("Cipher." + cryptMethod);
- if (providers == null || providers.length == 0)
- {
- try
- {
- // Initialize and add a security provider required for encryption
- final Class<?> clazz = Class.forName("com.sun.crypto.provider.SunJCE");
-
- final Provider provider = (Provider) clazz.getDeclaredConstructor().newInstance();
- Security.addProvider(provider);
- }
- catch (Exception ex)
- {
- throw new RuntimeException("Unable to load SunJCE service provider", ex);
- }
- }
- }
-
- @Override
- protected CryptData generateKey(Session session)
- {
- // generate new salt
- byte[] salt = SunJceCrypt.randomSalt();
-
- // generate new key
- String key = session.getId() + "." + UUID.randomUUID().toString();
-
- return new CryptData(key, salt);
- }
-
- @Override
- protected ICrypt createCrypt(CryptData keyParams)
- {
- SunJceCrypt crypt = new SunJceCrypt(cryptMethod, keyParams.salt, 1000);
- crypt.setKey(keyParams.key);
-
- return crypt;
- }
-
- static final class CryptData implements IClusterable
- {
- private static final long serialVersionUID = 1L;
-
- final String key;
-
- final byte[] salt;
-
- CryptData(String key, byte[] salt)
- {
- this.key = key;
- this.salt = salt;
- }
- }
-}
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/crypt/CryptFactoryCachingDecorator.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/NoCrypt.java
similarity index 53%
rename from wicket-util/src/main/java/org/apache/wicket/util/crypt/CryptFactoryCachingDecorator.java
rename to wicket-core/src/main/java/org/apache/wicket/core/util/crypt/NoCrypt.java
index 9fc71424b6..1fa3a992ec 100644
--- a/wicket-util/src/main/java/org/apache/wicket/util/crypt/CryptFactoryCachingDecorator.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/NoCrypt.java
@@ -14,40 +14,39 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.wicket.util.crypt;
-
-
-import org.apache.wicket.util.lang.Args;
+package org.apache.wicket.core.util.crypt;
/**
- * {@link ICryptFactory} decorator that caches the call to {@link ICryptFactory#newCrypt()}
- *
- * @author Igor Vaynberg (ivaynberg)
+ * An {@link ICrypt} that does <strong>not</strong> encrypt anything — it returns its input
+ * unchanged. It exists only for environments where the JRE ships without a suitable security
+ * provider.
+ * <p>
+ * <strong>Do not use this in production.</strong> It provides neither confidentiality nor
+ * integrity.
*/
-public class CryptFactoryCachingDecorator implements ICryptFactory
+public class NoCrypt implements ICrypt
{
- private final ICryptFactory delegate;
- private ICrypt cache;
+ @Override
+ public byte[] encrypt(byte[] plainBytes)
+ {
+ return plainBytes;
+ }
- /**
- * Construct.
- *
- * @param delegate
- * the crypt factory whose {@link ICryptFactory#newCrypt()} call will be cached
- */
- public CryptFactoryCachingDecorator(final ICryptFactory delegate)
+ @Override
+ public byte[] decrypt(byte[] encryptedBytes)
+ {
+ return encryptedBytes;
+ }
+
+ @Override
+ public String encryptUrlSafe(String plainText)
{
- Args.notNull(delegate, "delegate");
- this.delegate = delegate;
+ return plainText;
}
@Override
- public final ICrypt newCrypt()
+ public String decryptUrlSafe(String encryptedText)
{
- if (cache == null)
- {
- cache = delegate.newCrypt();
- }
- return cache;
+ return encryptedText;
}
}
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/crypt/NoCryptFactory.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/NoCryptFactory.java
similarity index 77%
rename from wicket-util/src/main/java/org/apache/wicket/util/crypt/NoCryptFactory.java
rename to wicket-core/src/main/java/org/apache/wicket/core/util/crypt/NoCryptFactory.java
index f33efceb36..2e4824018a 100644
--- a/wicket-util/src/main/java/org/apache/wicket/util/crypt/NoCryptFactory.java
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/NoCryptFactory.java
@@ -14,22 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.wicket.util.crypt;
-
+package org.apache.wicket.core.util.crypt;
/**
- * Crypt factory implementation for noop {@link NoCrypt} class
- *
- * @author Igor Vaynberg (ivaynberg)
+ * An {@link ICryptFactory} that returns a {@link NoCrypt} (no encryption). Only for environments
+ * without a suitable security provider; <strong>do not use in production</strong>.
*/
public class NoCryptFactory implements ICryptFactory
{
- private static final ICrypt crypt = new NoCrypt();
+ private final ICrypt crypt = new NoCrypt();
@Override
public ICrypt newCrypt()
{
return crypt;
}
-
}
diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/SchemeCrypt.java b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/SchemeCrypt.java
new file mode 100644
index 0000000000..d89a9a2bd6
--- /dev/null
+++ b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/SchemeCrypt.java
@@ -0,0 +1,117 @@
+/*
+ * 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.core.util.crypt;
+
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.crypto.SecretKey;
+
+import org.apache.wicket.util.lang.Args;
+
+/**
+ * The default {@link ICrypt} implementation. It writes self-describing, authenticated ciphertext
+ * of the form {@code marker(1) || scheme-payload} where the marker is the
+ * {@link ICryptScheme#id() id} of the scheme that produced the payload.
+ * <p>
+ * <strong>Encryption</strong> always uses the configured {@code encryptionScheme} (the strongest
+ * scheme). <strong>Decryption</strong> reads the marker, looks the scheme up in a whitelist and,
+ * if allowed, hands the rest to that scheme; otherwise it returns {@code null}. The marker is
+ * authenticated as associated data, so it cannot be altered to force a different scheme, and any
+ * scheme not in the whitelist is refused — together these prevent downgrade attacks.
+ * <p>
+ * To migrate to a stronger scheme, temporarily whitelist the old scheme (so existing data still
+ * decrypts) while setting the new scheme as {@code encryptionScheme} (so new data is upgraded),
+ * then drop the old scheme from the whitelist once the data has been rewritten.
+ */
+public class SchemeCrypt implements ICrypt
+{
+ private final SecretKey key;
+
+ private final SecureRandom random;
+
+ private final ICryptScheme encryptionScheme;
+
+ private final Map<Byte, ICryptScheme> allowedSchemes;
+
+ /**
+ * @param key
+ * the secret key
+ * @param random
+ * source of randomness for nonces
+ * @param encryptionScheme
+ * the scheme used for all encryption; always allowed for decryption too
+ * @param whitelistedSchemes
+ * additional schemes allowed for decryption (may be empty)
+ */
+ public SchemeCrypt(SecretKey key, SecureRandom random, ICryptScheme encryptionScheme,
+ Collection<? extends ICryptScheme> whitelistedSchemes)
+ {
+ this.key = Args.notNull(key, "key");
+ this.random = Args.notNull(random, "random");
+ this.encryptionScheme = Args.notNull(encryptionScheme, "encryptionScheme");
+ Args.notNull(whitelistedSchemes, "whitelistedSchemes");
+
+ Map<Byte, ICryptScheme> map = new HashMap<>();
+ // the encryption scheme is always allowed for decryption
+ map.put(encryptionScheme.id(), encryptionScheme);
+ for (ICryptScheme scheme : whitelistedSchemes)
+ {
+ map.put(scheme.id(), scheme);
+ }
+ this.allowedSchemes = map;
+ }
+
+ @Override
+ public byte[] encrypt(byte[] plainBytes)
+ {
+ Args.notNull(plainBytes, "plainBytes");
+
+ byte id = encryptionScheme.id();
+ byte[] aad = { id };
+ byte[] payload = encryptionScheme.encrypt(plainBytes, key, aad, random);
+
+ byte[] result = new byte[payload.length + 1];
+ result[0] = id;
+ System.arraycopy(payload, 0, result, 1, payload.length);
+ return result;
+ }
+
+ @Override
+ public byte[] decrypt(byte[] encryptedBytes)
+ {
+ if (encryptedBytes == null || encryptedBytes.length < 1)
+ {
+ return null;
+ }
+
+ byte id = encryptedBytes[0];
+ ICryptScheme scheme = allowedSchemes.get(id);
+ if (scheme == null)
+ {
+ // unknown or non-whitelisted scheme: refuse (downgrade protection)
+ return null;
+ }
+
+ byte[] aad = { id };
+ byte[] payload = Arrays.copyOfRange(encryptedBytes, 1, encryptedBytes.length);
+ return scheme.decrypt(payload, key, aad);
+ }
+}
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 32185a620b..9860db1a67 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
@@ -1,166 +1,178 @@
-/*
- * 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 java.security.SecureRandom;
-
-import javax.crypto.SecretKey;
-
-import org.apache.wicket.Application;
-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<>()
- {
- private static final long serialVersionUID = 1L;
- };
-
- private final ICrypter crypter;
-
- private final Application application;
-
- /**
- * @param delegate
- * store to delegate to
- * @param application
- * the application
- */
- public CryptingPageStore(IPageStore delegate, Application application)
- {
- super(delegate);
- this.application = Args.notNull(application, "application");
- crypter = newCrypter();
- }
-
- /**
- * 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(crypter
- .generateKey(application.getSecuritySettings().getRandomSupplier().getRandom())));
- }
-
- /**
- * Create a new {@link ICrypter}.
- */
- protected ICrypter newCrypter()
- {
- return application.getStoreSettings().getCrypter().get();
- }
-
- @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, crypter);
-
- 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, crypter,
- application.getSecuritySettings().getRandomSupplier().getRandom());
-
- page = new SerializedPage(page.getPageId(), serializedPage.getPageType(), encrypted);
-
- getDelegate().addPage(context, page);
- }
-
- private static class SessionData implements Serializable
- {
- private static final long serialVersionUID = 1L;
-
- private final SecretKey key;
-
- public SessionData(SecretKey key)
- {
- Args.notNull(key, "key");
-
- this.key = key;
- }
-
- public byte[] encrypt(byte[] decrypted, ICrypter crypter, SecureRandom random)
- {
- return crypter.encrypt(decrypted, key, random);
- }
-
- public byte[] decrypt(byte[] encrypted, ICrypter crypter)
- {
- return crypter.decrypt(encrypted, key);
- }
- }
-}
+/*
+ * 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 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.WicketRuntimeException;
+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;
+
+/**
+ * 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>
+ * <p>
+ * Each session gets its own random 256-bit AES key. Encryption uses the application's configured
+ * {@link SecuritySettings#getCryptScheme() crypt scheme}; a page that can no longer be decrypted
+ * (e.g. because the session key is gone or the data was tampered with) is treated as a cache miss.
+ */
+public class CryptingPageStore extends DelegatingPageStore
+{
+ private static final MetaDataKey<SessionData> KEY = new MetaDataKey<>()
+ {
+ private static final long serialVersionUID = 1L;
+ };
+
+ private final Application application;
+
+ /**
+ * @param delegate
+ * store to delegate to
+ * @param application
+ * the application
+ */
+ public CryptingPageStore(IPageStore delegate, Application application)
+ {
+ super(delegate);
+ this.application = Args.notNull(application, "application");
+ }
+
+ /**
+ * 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(generateKey()));
+ }
+
+ private SecretKey generateKey()
+ {
+ SecureRandom random = application.getSecuritySettings().getRandomSupplier().getRandom();
+ return new SecretKeySpec(CipherUtils.generateKey("AES", 256, random).getEncoded(), "AES");
+ }
+
+ /**
+ * Builds an {@link ICrypt} bound to the current session's key and the application's crypto
+ * policy (encryption scheme + decryption whitelist).
+ */
+ private ICrypt getCrypt(IPageContext context)
+ {
+ SecuritySettings settings = application.getSecuritySettings();
+ return new SchemeCrypt(getSessionData(context).getKey(),
+ settings.getRandomSupplier().getRandom(), settings.getCryptScheme(),
+ settings.getWhitelistedCryptSchemes());
+ }
+
+ @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 = getCrypt(context).decrypt(encrypted);
+
+ if (decrypted == null)
+ {
+ // the page could not be decrypted (e.g. a new session key or tampered data):
+ // treat it as if it were no longer present
+ return null;
+ }
+
+ 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 = getCrypt(context).encrypt(decrypted);
+
+ page = new SerializedPage(page.getPageId(), serializedPage.getPageType(), encrypted);
+
+ getDelegate().addPage(context, page);
+ }
+
+ private static class SessionData implements Serializable
+ {
+ private static final long serialVersionUID = 1L;
+
+ private final SecretKey key;
+
+ public SessionData(SecretKey key)
+ {
+ this.key = Args.notNull(key, "key");
+ }
+
+ public SecretKey getKey()
+ {
+ return key;
+ }
+ }
+}
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
deleted file mode 100644
index bde5d79448..0000000000
--- a/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/DefaultCrypter.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * 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 javax.crypto.spec.IvParameterSpec;
-
-import org.apache.wicket.WicketRuntimeException;
-
-/**
- * Default encryption and decryption implementation, using AES-256 in CBC mode.
- * <p>
- * <strong>This implementation is not authenticated and provides confidentiality only.</strong> CBC
- * ciphertext is malleable and its integrity is not verified on decryption, so an attacker who can
- * write to the underlying store can alter the stored bytes and have the result passed to the
- * deserializer. Enabling encryption with this crypter therefore hides the contents of stored pages,
- * but does not make the page store safe against tampering - the underlying store must still be
- * treated as trusted, private storage.
- * <p>
- * Use {@link GCMSIVCrypter} instead where the stored bytes need to be tamper-evident as well as
- * confidential. It is slower (see its javadoc) and requires Bouncy Castle, which is why this
- * implementation remains the default.
- *
- * @see GCMSIVCrypter
- * @see ICrypter
- */
-public class DefaultCrypter implements ICrypter
-{
- protected Cipher getCipher() throws GeneralSecurityException
- {
- return Cipher.getInstance("AES/CBC/PKCS5Padding");
- }
-
- @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[] 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, SecretKey key)
- {
- 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/GCMSIVCrypter.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/GCMSIVCrypter.java
deleted file mode 100644
index 456b3cffe6..0000000000
--- a/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/GCMSIVCrypter.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.
- * <p>
- * Because GCM-SIV is an AEAD mode, this implementation provides integrity as well as
- * confidentiality: modified or substituted ciphertext fails to decrypt instead of being handed to
- * the deserializer. It is therefore the implementation to choose when the page store needs to be
- * tamper-evident and not merely unreadable - unlike the unauthenticated {@link DefaultCrypter},
- * which is the default. Note that the key is held in the session, so this protects against parties
- * who can read or write the stored bytes, not against one who already controls the session.
- * <p>
- * This implementation requires Bouncy Castle, which is an optional dependency of {@code
- * wicket-core} and must be added explicitly. 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.
- *
- * @see DefaultCrypter
- * @see ICrypter
- */
-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/pageStore/crypt/ICrypter.java b/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/ICrypter.java
deleted file mode 100644
index 3595b9b6da..0000000000
--- a/wicket-core/src/main/java/org/apache/wicket/pageStore/crypt/ICrypter.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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;
-import java.security.SecureRandom;
-
-import javax.crypto.SecretKey;
-
-/**
- * An encrypter and decrypter of pages.
- * <p>
- * Implementations are not required to provide <em>authenticated</em> encryption, and callers must
- * not assume that they do. An unauthenticated implementation gives confidentiality only: it hides
- * the contents of a serialized page, but it does not detect modification of the stored bytes, so
- * tampered ciphertext may still be handed to the deserializer. Of the implementations shipped with
- * Wicket, {@link GCMSIVCrypter} is authenticated and {@link DefaultCrypter} - the default - is not.
- * <p>
- * If you implement this interface and want tamper detection, use an AEAD cipher mode (such as
- * GCM, GCM-SIV or CCM) rather than adding encryption on top of an unauthenticated mode. See
- * {@code SECURITY.md} for the trust assumptions Wicket makes about the page store.
- *
- * @see org.apache.wicket.pageStore.CryptingPageStore
- * @see org.apache.wicket.settings.StoreSettings#setCrypter(java.util.function.Supplier)
- */
-public interface ICrypter {
- SecretKey generateKey(SecureRandom random);
-
- byte[] encrypt(byte[] bytes, SecretKey key, SecureRandom random);
-
- byte[] decrypt(byte[] bytes, SecretKey key);
-}
\ No newline at end of file
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 fdd9782538..ed12b61b39 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
@@ -28,11 +28,22 @@ import org.apache.wicket.coep.CrossOriginEmbedderPolicyConfiguration;
import org.apache.wicket.coep.CrossOriginEmbedderPolicyConfiguration.CoepMode;
import org.apache.wicket.coop.CrossOriginOpenerPolicyConfiguration;
import org.apache.wicket.coop.CrossOriginOpenerPolicyConfiguration.CoopMode;
+import java.security.SecureRandom;
+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;
-import org.apache.wicket.core.util.crypt.KeyInSessionSunJceCryptFactory;
-import org.apache.wicket.util.crypt.ICryptFactory;
-import org.apache.wicket.util.crypt.SunJceCrypt;
+import org.apache.wicket.core.util.crypt.AesGcmCryptScheme;
+import org.apache.wicket.core.util.crypt.ICrypt;
+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;
/**
@@ -58,6 +69,12 @@ public class SecuritySettings
/** factory for creating crypt objects */
private ICryptFactory cryptFactory;
+ /** the scheme used to encrypt (the strongest configured scheme) */
+ private ICryptScheme cryptScheme;
+
+ /** the schemes accepted for decryption (downgrade protection) */
+ private Collection<ICryptScheme> whitelistedCryptSchemes;
+
/** supplier of random data and SecureRandom */
private ISecureRandomSupplier randomSupplier;
@@ -117,20 +134,85 @@ public class SecuritySettings
}
/**
- * Returns the {@link ICryptFactory}. If no factory is set, a {@link KeyInSessionSunJceCryptFactory}
+ * Returns the {@link ICryptFactory}. If no factory is set, a {@link KeyInSessionCryptFactory}
* is used.
- *
+ *
* @return crypt factory used to generate crypt objects
*/
public synchronized ICryptFactory getCryptFactory()
{
if (cryptFactory == null)
{
- cryptFactory = new KeyInSessionSunJceCryptFactory();
+ cryptFactory = new KeyInSessionCryptFactory();
}
return cryptFactory;
}
+ /**
+ * Returns the {@link ICryptScheme} used to <em>encrypt</em> (the strongest configured scheme).
+ * If none is set, {@link AesGcmCryptScheme} (JDK-native authenticated AES-256-GCM) is used.
+ *
+ * @return the encryption scheme
+ */
+ public synchronized ICryptScheme getCryptScheme()
+ {
+ if (cryptScheme == null)
+ {
+ cryptScheme = new AesGcmCryptScheme();
+ }
+ return cryptScheme;
+ }
+
+ /**
+ * Sets the {@link ICryptScheme} used for all encryption. This should be the strongest scheme
+ * available; existing data encrypted with an older scheme keeps decrypting only while that
+ * scheme is whitelisted (see {@link #setWhitelistedCryptSchemes(Collection)}).
+ *
+ * @param cryptScheme
+ * the encryption scheme, must not be null
+ * @return {@code this} object for chaining
+ */
+ public synchronized SecuritySettings setCryptScheme(ICryptScheme cryptScheme)
+ {
+ this.cryptScheme = Args.notNull(cryptScheme, "cryptScheme");
+ return this;
+ }
+
+ /**
+ * Returns the schemes accepted for <em>decryption</em>. Ciphertext whose scheme marker is not
+ * in this set is refused, which protects against downgrade attacks. The encryption scheme
+ * (see {@link #getCryptScheme()}) is always accepted for decryption regardless of this set.
+ * If none is set, only the encryption scheme is accepted.
+ *
+ * @return the whitelisted decryption schemes
+ */
+ public synchronized Collection<ICryptScheme> getWhitelistedCryptSchemes()
+ {
+ if (whitelistedCryptSchemes == null)
+ {
+ return List.of(getCryptScheme());
+ }
+ return whitelistedCryptSchemes;
+ }
+
+ /**
+ * Sets the schemes accepted for decryption (downgrade protection). To migrate to a stronger
+ * scheme, temporarily include the old scheme here (so existing data still decrypts) while
+ * setting the new scheme via {@link #setCryptScheme(ICryptScheme)}; drop the old scheme once
+ * the data has been rewritten.
+ *
+ * @param whitelistedCryptSchemes
+ * the accepted decryption schemes, must not be null
+ * @return {@code this} object for chaining
+ */
+ public synchronized SecuritySettings setWhitelistedCryptSchemes(
+ Collection<ICryptScheme> whitelistedCryptSchemes)
+ {
+ this.whitelistedCryptSchemes = Args.notNull(whitelistedCryptSchemes,
+ "whitelistedCryptSchemes");
+ return this;
+ }
+
/**
* Returns the {@link ISecureRandomSupplier} to use for secure random data. If no custom
* supplier is set, a {@link DefaultSecureRandomSupplier} is used.
@@ -270,12 +352,19 @@ public class SecuritySettings
*
* @return Returns the authentication strategy.
*/
- @SuppressWarnings("deprecation")
public IAuthenticationStrategy getAuthenticationStrategy()
{
if (authenticationStrategy == null)
{
- authenticationStrategy = new DefaultAuthenticationStrategy("LoggedIn", new SunJceCrypt(SunJceCrypt.randomSalt(), 17));
+ // a "remember me" cookie must be decryptable across sessions, so it uses an
+ // 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");
+ ICrypt crypt = new SchemeCrypt(key, random, getCryptScheme(),
+ getWhitelistedCryptSchemes());
+ authenticationStrategy = new DefaultAuthenticationStrategy("LoggedIn", crypt);
}
return authenticationStrategy;
}
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 b25315a5dc..feb83438a2 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,12 +18,9 @@ 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;
@@ -53,8 +50,6 @@ public class StoreSettings
private boolean asynchronous = true;
private boolean encrypted = false;
-
- private Supplier<ICrypter> crypter = DefaultCrypter::new;
/**
* Construct.
@@ -190,6 +185,17 @@ public class StoreSettings
/**
* Sets a flag whether to wrap the configured {@link org.apache.wicket.pageStore.IPageStore} with
* {@link org.apache.wicket.pageStore.CryptingPageStore}.
+ * <p>
+ * Pages are encrypted with a random 256 bit key per session, using the authenticated scheme
+ * configured on
+ * {@link SecuritySettings#setCryptScheme(org.apache.wicket.core.util.crypt.ICryptScheme)}, so
+ * the stored bytes are tamper-evident as well as confidential: a modified page fails to decrypt
+ * and is treated as no longer present rather than being handed to the deserializer.
+ * <p>
+ * Because the key lives in the session, this protects the stored pages against parties who can
+ * read or write the underlying store, not against one who already controls the session. The
+ * store should therefore still be treated as trusted, private storage - see {@code SECURITY.md}
+ * for the trust assumptions Wicket makes about it.
*
* @param encrypted
* {@code true} to encrypt, {@code false} - otherwise
@@ -208,33 +214,4 @@ public class StoreSettings
{
return encrypted;
}
-
- /**
- * Sets the supplier for the {@link ICrypter} used by a
- * {@link org.apache.wicket.pageStore.CryptingPageStore}.
- * <p>
- * The default {@link DefaultCrypter} is not authenticated and gives confidentiality only. Set
- * {@link org.apache.wicket.pageStore.crypt.GCMSIVCrypter} here if the stored bytes must also be
- * tamper-evident.
- *
- * @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}, which is not authenticated - see
- * {@link #setCrypter(Supplier)}.
- */
- public Supplier<ICrypter> getCrypter()
- {
- return crypter;
- }
}
diff --git a/wicket-examples/src/main/java/org/apache/wicket/examples/WicketExampleApplication.java b/wicket-examples/src/main/java/org/apache/wicket/examples/WicketExampleApplication.java
index dc85c390c6..d80c622dbb 100644
--- a/wicket-examples/src/main/java/org/apache/wicket/examples/WicketExampleApplication.java
+++ b/wicket-examples/src/main/java/org/apache/wicket/examples/WicketExampleApplication.java
@@ -22,7 +22,7 @@ import org.apache.wicket.request.cycle.IRequestCycleListener;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.resource.CssUrlReplacer;
-import org.apache.wicket.util.crypt.NoCrypt;
+import org.apache.wicket.core.util.crypt.NoCrypt;
/**
diff --git a/wicket-migration/src/main/resources/META-INF/rewrite/wicket.yml b/wicket-migration/src/main/resources/META-INF/rewrite/wicket.yml
index f05e61d261..5d4a66800f 100644
--- a/wicket-migration/src/main/resources/META-INF/rewrite/wicket.yml
+++ b/wicket-migration/src/main/resources/META-INF/rewrite/wicket.yml
@@ -45,3 +45,44 @@ recipeList:
artifactId: "*"
newVersion: 10.x
---
+# https://cwiki.apache.org/confluence/display/WICKET/Migration+to+Wicket+11.0
+#
+# The cryptography subsystem was unified and rebuilt in Wicket 11. The two former
+# interfaces (org.apache.wicket.util.crypt.ICrypt and
+# org.apache.wicket.pageStore.crypt.ICrypter) are replaced by a single
+# org.apache.wicket.core.util.crypt.ICrypt, and the following classes were removed with no
+# drop-in replacement (encryption now always uses an authenticated scheme selected via
+# org.apache.wicket.settings.SecuritySettings#setCryptScheme):
+# SunJceCrypt, AbstractCrypt, TrivialCrypt, CryptFactoryCachingDecorator,
+# AESCrypt, AbstractJceCrypt, KeyInSessionSunJceCryptFactory,
+# AbstractKeyInSessionCryptFactory, DefaultCrypter, GCMSIVCrypter, ICrypter.
+# These require manual migration; see the guide above. Data encrypted by Wicket 10 cannot
+# be decrypted by Wicket 11 (per-session keys and formats change).
+type: specs.openrewrite.org/v1beta/recipe
+name: org.apache.wicket.MigrateToWicket11
+displayName: Migrate to Wicket 11.x
+description: Migrates Wicket 10.x to Wicket 11.x, as well as Java 21.
+recipeList:
+ - org.openrewrite.java.migrate.UpgradeToJava21
+ - org.openrewrite.java.ChangeType:
+ oldFullyQualifiedTypeName: org.apache.wicket.util.crypt.ICrypt
+ newFullyQualifiedTypeName: org.apache.wicket.core.util.crypt.ICrypt
+ ignoreDefinition: "True"
+ - org.openrewrite.java.ChangeType:
+ oldFullyQualifiedTypeName: org.apache.wicket.util.crypt.ICryptFactory
+ newFullyQualifiedTypeName: org.apache.wicket.core.util.crypt.ICryptFactory
+ ignoreDefinition: "True"
+ - org.openrewrite.java.ChangeType:
+ oldFullyQualifiedTypeName: org.apache.wicket.util.crypt.NoCrypt
+ newFullyQualifiedTypeName: org.apache.wicket.core.util.crypt.NoCrypt
+ ignoreDefinition: "True"
+ - org.openrewrite.java.ChangeType:
+ oldFullyQualifiedTypeName: org.apache.wicket.util.crypt.NoCryptFactory
+ newFullyQualifiedTypeName: org.apache.wicket.core.util.crypt.NoCryptFactory
+ ignoreDefinition: "True"
+ - org.openrewrite.maven.UpgradeDependencyVersion:
+ groupId: org.apache.wicket
+ artifactId: "*"
+ newVersion: 11.x
+ versionPattern: -SNAPSHOT
+---
diff --git a/wicket-user-guide/src/main/asciidoc/internals/pagestoring.adoc b/wicket-user-guide/src/main/asciidoc/internals/pagestoring.adoc
index f98f7d1d4e..96005ff1dd 100644
--- a/wicket-user-guide/src/main/asciidoc/internals/pagestoring.adoc
+++ b/wicket-user-guide/src/main/asciidoc/internals/pagestoring.adoc
@@ -38,28 +38,15 @@ i.e. this is the serialized page instance (data) plus additional information nee
* *AsynchronousPageStore:* The role of _AsynchronousPageStore_ is to detach the http worker thread from waiting for the write of the page bytes to the disk. To disable it use: _org.apache.wicket.settings.StoreSettings.setAsynchronous(false)_. _AsynchronousPageStore_ can delay the storage of page's bytes for at most _org.apache.wicket.settings.StoreSettings.setAsynchronousQueueCapacity(int)_ pages. If this capacity is exceeded then the page's bytes are written synchronously to the backing [...]
-* *CryptingPageStore:* page instances might contain sensible information, therefore it's important to have the chance to encrypt their content before persist them on disk. _CryptingPageStore_ encrypts _SerializedPage_'s with a 256 bit AES key, held in the session, before passing them to the underlying _DiskPageStore_. By default this stage is disabled and not added to the default chain. To change this behavior we can use _org.apache.wicket.settings.StoreSettings.setEncrypted_. What that [...]
+* *CryptingPageStore:* page instances might contain sensible information, therefore it's important to have the chance to encrypt their content before persist them on disk. _CryptingPageStore_ encrypts _SerializedPage_'s with an authenticated cipher (by default JDK-native AES-256-GCM) before passing them to the underling _DiskPageStore_, using a random 256 bit key per session. Because the encryption is authenticated, tampering is detected on decryption (a modified page is treated as if it [...]
++
+The cipher (the "scheme") is configured application-wide via _org.apache.wicket.settings.SecuritySettings.setCryptScheme_ and defaults to _org.apache.wicket.core.util.crypt.AesGcmCryptScheme_. An _AesGcmSivCryptScheme_ (AES-256-GCM-SIV, nonce-misuse resistant) is also available but requires Bouncy Castle on the classpath. Each ciphertext starts with a one-byte marker identifying its scheme; on decryption the marker must match one of the schemes accepted by _SecuritySettings.setWhiteliste [...]
++
+Note that the key lives in the user's HTTP session, so the encryption protects the stored pages against parties who can read or write the underlying store - it does not protect them against someone who already controls the session. Wicket deserializes whatever it reads back from the store, so the store itself should still be treated as trusted, private storage.
* *DiskPageStore:* stores _SerializedPage_ on a session-scoped file on disk. The location of the folder where the files are stored is configurable via _org.apache.wicket.settings.StoreSettings.setFileStoreFolder(File)_, by default the web container's work folder is used (ServletContext attribute 'javax.servlet.context.tempdir'). In this folder a sub-folder is created named _'applicationName-filestore'_.
This folder contains a sub-folder for each active http session. This session folder contains a single file named 'data' which contains the bytes for the pages. The size of this 'data' file is configurable via _org.apache.wicket.settings.StoreSettings.setMaxSizePerSession(Bytes)_. When this size is exceeded the newly stored files overwrite the oldest ones.
-[#pagestore-choosing-a-crypter]
-=== Choosing a crypter for CryptingPageStore
-
-When page store encryption is enabled, the actual encryption is performed by an implementation of _org.apache.wicket.pageStore.crypt.ICrypter_, configurable with _org.apache.wicket.settings.StoreSettings.setCrypter_. Which implementation we pick determines what the encryption protects against, and the difference matters because Wicket deserializes whatever it reads back from the store:
-
-* _DefaultCrypter_ (AES-256-CBC) is the default. It is *not* authenticated, so it provides confidentiality only: the stored pages are unreadable, but CBC ciphertext is malleable and its integrity is not verified on decryption, so someone able to write to the store can still influence what gets deserialized.
-* _GCMSIVCrypter_ (AES-256-GCM-SIV) is authenticated. Modified or substituted ciphertext fails to decrypt instead of reaching the deserializer, which makes the stored bytes tamper-evident as well as confidential. It requires Bouncy Castle - an optional dependency of _wicket-core_ that we must add ourselves - and is noticeably slower, which is why it is not the default.
-
-So if we enable encryption because the store lives somewhere we do not fully control, rather than merely to keep page contents private, we should select the authenticated crypter explicitly:
-
-[source,java]
-----
-getStoreSettings().setEncrypted(true).setCrypter(GCMSIVCrypter::new);
-----
-
-In both cases the key is held in the session, so encryption protects the pages against parties who can read or write the stored bytes - not against one who already controls the session. A custom _ICrypter_ only provides tamper detection if the cipher it returns uses an AEAD mode.
-
=== InMemoryPageStore
An alternative _IPageStore_ we can use in a custom chain is _org.apache.wicket.pageStore.InMemoryPageStore_. This implementation stores pages in a session-relative variable which can be limited by size or by page instance number.
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/crypt/AbstractCrypt.java b/wicket-util/src/main/java/org/apache/wicket/util/crypt/AbstractCrypt.java
deleted file mode 100644
index c68f124d98..0000000000
--- a/wicket-util/src/main/java/org/apache/wicket/util/crypt/AbstractCrypt.java
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * 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.util.crypt;
-
-import java.io.UnsupportedEncodingException;
-import java.security.GeneralSecurityException;
-import java.util.Base64;
-import java.util.UUID;
-
-import javax.crypto.Cipher;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-
-/**
- * Abstract base class for JCE based ICrypt implementations.
- *
- * @author Juergen Donnerstag
- */
-public abstract class AbstractCrypt implements ICrypt
-{
- /** Encoding used to convert java String from and to byte[] */
- private static final String CHARACTER_ENCODING = "UTF-8";
-
- /** Log. */
- private static final Logger log = LoggerFactory.getLogger(AbstractCrypt.class);
-
- /** Key used to de-/encrypt the data */
- private String encryptionKey;
-
- /**
- * Constructor
- */
- public AbstractCrypt()
- {
- this.encryptionKey = UUID.randomUUID().toString();
- }
-
- /**
- * Decrypts a string into a string.
- *
- * @param text
- * text to decrypt
- * @return the decrypted text
- */
- @Override
- public final String decryptUrlSafe(final String text)
- {
- try
- {
- byte[] decoded = java.util.Base64.getUrlDecoder().decode(text);
- return new String(decryptByteArray(decoded), CHARACTER_ENCODING);
- }
- catch (Exception ex)
- {
- log.debug("Error decoding text: " + text, ex);
- return null;
- }
- }
-
- /**
- * Encrypt a string into a string using URL safe Base64 encoding.
- *
- * @param plainText
- * text to encrypt
- * @return encrypted string
- */
- @Override
- public final String encryptUrlSafe(final String plainText)
- {
- try
- {
- byte[] encrypted = encryptStringToByteArray(plainText);
- Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding();
- byte[] encoded = encoder.encode(encrypted);
- return new String(encoded, CHARACTER_ENCODING);
- }
- catch (GeneralSecurityException e)
- {
- log.error("Unable to encrypt text '" + plainText + "'", e);
- return null;
- }
- catch (UnsupportedEncodingException e)
- {
- log.error("Unable to encrypt text '" + plainText + "'", e);
- return null;
- }
- }
-
- /**
- * Get encryption private key
- *
- * @return encryption private key
- */
- public String getKey()
- {
- return encryptionKey;
- }
-
- /**
- * Set encryption private key
- *
- * @param key
- * private key to make de-/encryption unique
- */
- public void setKey(final String key)
- {
- encryptionKey = key;
- }
-
- /**
- * Crypts the given byte array
- *
- * @param input
- * byte array to be crypted
- * @param mode
- * crypt mode
- * @return the input crypted. Null in case of an error
- * @throws GeneralSecurityException
- */
- protected abstract byte[] crypt(final byte[] input, final int mode)
- throws GeneralSecurityException;
-
- /**
- * Decrypts an encrypted, but Base64 decoded byte array into a byte array.
- *
- * @param encrypted
- * byte array to decrypt
- * @return the decrypted text
- */
- private byte[] decryptByteArray(final byte[] encrypted)
- {
- try
- {
- return crypt(encrypted, Cipher.DECRYPT_MODE);
- }
- catch (GeneralSecurityException e)
- {
- throw new RuntimeException(
- "Unable to decrypt the text '" + new String(encrypted) + "'", e);
- }
- }
-
- /**
- * Encrypts the given text into a byte array.
- *
- * @param plainText
- * text to encrypt
- * @return the string encrypted
- * @throws GeneralSecurityException
- */
- private byte[] encryptStringToByteArray(final String plainText)
- throws GeneralSecurityException
- {
- try
- {
- return crypt(plainText.getBytes(CHARACTER_ENCODING), Cipher.ENCRYPT_MODE);
- }
- catch (UnsupportedEncodingException ex)
- {
- throw new RuntimeException(ex.getMessage());
- }
- }
-}
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/crypt/ICrypt.java b/wicket-util/src/main/java/org/apache/wicket/util/crypt/ICrypt.java
deleted file mode 100644
index 5e8e517e82..0000000000
--- a/wicket-util/src/main/java/org/apache/wicket/util/crypt/ICrypt.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.util.crypt;
-
-/**
- * Encryption and decryption implementations are accessed through this interface. It provide some
- * simple means to encrypt and decrypt strings, like passwords etc.. It depends on the
- * implementation itself which algorithms are used to en-/decrypt the data.
- * <p>
- * If you value the privacy of your websites users, then please consider using a one-way encryption
- * algorithm instead of the Wicket provided ICrypt implementations. The intention of these
- * encryption facilities is to keep passwords private when stored in cookies or in the session.The
- * implementation of the encryption algorithm may change between releases. As such, this interface
- * and its implementations are not intended and should not be used as an encryption facility for
- * persistent values.
- *
- * @author Juergen Donnerstag
- */
-public interface ICrypt
-{
- /**
- * Decrypts a string using URL and filename safe Base64 decoding.
- *
- * @param text
- * the text to decrypt
- * @return the decrypted string.
- * @since 1.2
- */
- String decryptUrlSafe(final String text);
-
- /**
- * Encrypts a string using URL and filename safe Base64 encoding.
- *
- * @param plainText
- * @return encrypted string
- * @since 1.2
- */
- String encryptUrlSafe(final String plainText);
-}
\ No newline at end of file
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/crypt/NoCrypt.java b/wicket-util/src/main/java/org/apache/wicket/util/crypt/NoCrypt.java
deleted file mode 100644
index 1e7a800ca3..0000000000
--- a/wicket-util/src/main/java/org/apache/wicket/util/crypt/NoCrypt.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.util.crypt;
-
-
-/**
- * Due to legal reasons in some countries the JRE is shipped without a security provider. As a
- * convenience solution, we provide a default implementation which does not encrypt/decrypt the
- * data. It does not modify the data at all. Thus we strongly recommend not to use it for production
- * sites.
- *
- * @author Juergen Donnerstag
- */
-public class NoCrypt implements ICrypt
-{
- /**
- * Constructor
- */
- public NoCrypt()
- {
- }
-
- /**
- * Decrypts a string into a string.
- *
- * @param text
- * text to decrypt
- * @return the decrypted text
- */
- @Override
- public final String decryptUrlSafe(final String text)
- {
- return text;
- }
-
- /**
- * Encrypt a string into a string
- *
- * @param plainText
- * text to encrypt
- * @return encrypted string
- */
- @Override
- public final String encryptUrlSafe(final String plainText)
- {
- return plainText;
- }
-}
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/crypt/SunJceCrypt.java b/wicket-util/src/main/java/org/apache/wicket/util/crypt/SunJceCrypt.java
deleted file mode 100644
index 52be417561..0000000000
--- a/wicket-util/src/main/java/org/apache/wicket/util/crypt/SunJceCrypt.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * 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.util.crypt;
-
-import java.security.GeneralSecurityException;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.security.spec.AlgorithmParameterSpec;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.KeySpec;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.PBEKeySpec;
-import javax.crypto.spec.PBEParameterSpec;
-
-import org.apache.wicket.util.lang.Args;
-
-
-/**
- * Provide some simple means to encrypt and decrypt strings such as passwords. The whole
- * implementation is based around Sun's security providers and uses the <a
- * href="http://www.ietf.org/rfc/rfc2898.txt">PBEWithMD5AndDES</a> method to encrypt and decrypt the
- * data.
- *
- * @author Juergen Donnerstag
- */
-public class SunJceCrypt extends AbstractCrypt
-{
- private static final SecureRandom SECURE_RANDOM = new SecureRandom();
- /** Name of the default encryption method */
- public static final String DEFAULT_CRYPT_METHOD = "PBEWithMD5AndDES";
-
- /** The name of encryption method (cipher) */
- private final String cryptMethod;
-
- private final int iterationCount;
-
- private final byte[] salt;
-
- /**
- * Constructor.
- *
- * @param salt
- * salt for encryption
- * @param iterationCount
- * iteration count
- */
- public SunJceCrypt(byte[] salt, int iterationCount)
- {
- this(DEFAULT_CRYPT_METHOD, salt, iterationCount);
- }
-
- /**
- * Constructor that uses a custom encryption method (cipher).
- * You may need to override {@link #createKeySpec()} and/or
- * {@link #createParameterSpec()} for the custom cipher.
- *
- * @param cryptMethod
- * the name of encryption method (the cipher)
- * @param salt
- * salt for encryption
- * @param iterationCount
- * iteration count
- */
- public SunJceCrypt(String cryptMethod, byte[] salt, int iterationCount)
- {
- this.cryptMethod = Args.notNull(cryptMethod, "Crypt method");
- this.salt = Args.notNull(salt, "salt");
- this.iterationCount = Args.withinRange(1, Integer.MAX_VALUE, iterationCount, "iterationCount");
- }
-
- /**
- * Crypts the given byte array
- *
- * @param input
- * byte array to be encrypted
- * @param mode
- * crypt mode
- * @return the input crypted. Null in case of an error
- * @throws GeneralSecurityException
- */
- @Override
- protected byte[] crypt(final byte[] input, final int mode)
- throws GeneralSecurityException
- {
- SecretKey key = generateSecretKey();
- AlgorithmParameterSpec spec = createParameterSpec();
- Cipher ciph = createCipher(key, spec, mode);
- return ciph.doFinal(input);
- }
-
- /**
- * Creates the {@link javax.crypto.Cipher} that will do the de-/encryption.
- *
- * @param key
- * the secret key to use
- * @param spec
- * the parameters spec to use
- * @param mode
- * the mode ({@link javax.crypto.Cipher#ENCRYPT_MODE} or {@link javax.crypto.Cipher#DECRYPT_MODE})
- * @return the cipher that will do the de-/encryption
- * @throws GeneralSecurityException
- */
- protected Cipher createCipher(SecretKey key, AlgorithmParameterSpec spec, int mode) throws GeneralSecurityException
- {
- Cipher cipher = Cipher.getInstance(cryptMethod);
- cipher.init(mode, key, spec);
- return cipher;
- }
-
- /**
- * Generate the de-/encryption key.
- * <p>
- * Note: if you don't provide your own encryption key, the implementation will use a default. Be
- * aware that this is potential security risk. Thus make sure you always provide your own one.
- *
- * @return secretKey the security key generated
- * @throws NoSuchAlgorithmException
- * unable to find encryption algorithm specified
- * @throws InvalidKeySpecException
- * invalid encryption key
- */
- protected SecretKey generateSecretKey() throws NoSuchAlgorithmException,
- InvalidKeySpecException
- {
- SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(cryptMethod);
- KeySpec spec = createKeySpec();
- return keyFactory.generateSecret(spec);
- }
-
- /**
- * @return the parameter spec to be used for the configured crypt method
- */
- protected AlgorithmParameterSpec createParameterSpec()
- {
- return new PBEParameterSpec(salt, iterationCount);
- }
-
- /**
- * @return the key spec to be used for the configured crypt method
- */
- protected KeySpec createKeySpec()
- {
- return new PBEKeySpec(getKey().toCharArray());
- }
-
- /**
- * Create a random salt to be used for this crypt.
- *
- * @return salt, always 8 bytes long
- */
- public static byte[] randomSalt()
- {
- // must be 8 bytes - for anything else PBES1Core throws
- // InvalidAlgorithmParameterException: Salt must be 8 bytes long
- byte[] salt = new byte[8];
- SECURE_RANDOM.nextBytes(salt);
- return salt;
- }
-}
diff --git a/wicket-util/src/main/java/org/apache/wicket/util/crypt/TrivialCrypt.java b/wicket-util/src/main/java/org/apache/wicket/util/crypt/TrivialCrypt.java
deleted file mode 100644
index 5aab8e75b8..0000000000
--- a/wicket-util/src/main/java/org/apache/wicket/util/crypt/TrivialCrypt.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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.util.crypt;
-
-import java.security.GeneralSecurityException;
-
-/**
- * THIS CLASS IS FOR TESTING PURPOSES ONLY. DO NOT USE IT IN PRODUCTION CODE!
- *
- * @author Jonathan Locke
- */
-public class TrivialCrypt extends AbstractCrypt
-{
- @Override
- protected byte[] crypt(final byte[] input, final int mode) throws GeneralSecurityException
- {
- final byte[] result = new byte[input.length];
- for (int i = 0; i < input.length; i++)
- {
- result[i] = (byte)(input[i] ^ 0xff);
- }
- return result;
- }
-}
diff --git a/wicket-util/src/test/java/org/apache/wicket/util/crypt/SunJceCryptTest.java b/wicket-util/src/test/java/org/apache/wicket/util/crypt/SunJceCryptTest.java
deleted file mode 100644
index 4d529dacfc..0000000000
--- a/wicket-util/src/test/java/org/apache/wicket/util/crypt/SunJceCryptTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.util.crypt;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Assumptions;
-import org.junit.jupiter.api.Test;
-
-import java.security.GeneralSecurityException;
-import java.security.NoSuchAlgorithmException;
-
-import javax.crypto.Cipher;
-
-@SuppressWarnings("javadoc")
-public class SunJceCryptTest
-{
- /**
- * Default encryption uses {@value org.apache.wicket.util.crypt.SunJceCrypt#DEFAULT_CRYPT_METHOD}
- */
- @Test
- public void defaultEncryption() throws GeneralSecurityException
- {
- SunJceCrypt crypt = new SunJceCrypt(SunJceCrypt.randomSalt(), 1);
- String input = "input";
- byte[] encrypted = crypt.crypt(input.getBytes(), Cipher.ENCRYPT_MODE);
-
- byte[] decrypted = crypt.crypt(encrypted, Cipher.DECRYPT_MODE);
- assertEquals(new String(decrypted), input);
- }
-
- /**
- * Uses <em>PBEWithMD5AndTripleDES</em> if unlimited cryptography is installed
- */
- @Test
- public void customPBEEncryption() throws GeneralSecurityException
- {
- boolean unlimitedStrengthJurisdictionPolicyInstalled = isUnlimitedStrengthJurisdictionPolicyInstalled();
- Assumptions.assumeTrue(unlimitedStrengthJurisdictionPolicyInstalled);
-
- SunJceCrypt crypt = new SunJceCrypt("PBEWithMD5AndTripleDES", SunJceCrypt.randomSalt(), 1);
- String input = "input";
- byte[] encrypted = crypt.crypt(input.getBytes(), Cipher.ENCRYPT_MODE);
-
- byte[] decrypted = crypt.crypt(encrypted, Cipher.DECRYPT_MODE);
- assertEquals(new String(decrypted), input);
- }
-
- /**
- * Checks whether Oracle Unlimited Strength Jurisdiction Policy is installed
- * Based on http://stackoverflow.com/a/8607735
- *
- * @return {@code true} if Unlimited Strength Jurisdiction Policy is installed
- * @throws NoSuchAlgorithmException
- */
- public static boolean isUnlimitedStrengthJurisdictionPolicyInstalled() throws NoSuchAlgorithmException
- {
- return Cipher.getMaxAllowedKeyLength("AES") == Integer.MAX_VALUE;
- }
-}
(wicket) 05/06: WICKET-7190 Verify the upload token by decrypting it
Posted by pa...@apache.org.
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 e1c2f265082461f629daee38ec8806c9139ceeca
Author: Emond Papegaaij <em...@topicus.nl>
AuthorDate: Tue Aug 4 13:14:36 2026 +0200
WICKET-7190 Verify the upload token by decrypting it
The signed upload settings introduced in 483b83ca62 were validated by
re-encrypting the expected settings and comparing the resulting ciphertext
to the token supplied by the client. That only worked because the default
crypt at the time was deterministic PBE: the same plaintext always produced
the same ciphertext.
SchemeCrypt encrypts with a fresh random nonce per message, as an AEAD mode
must, so no two encryptions of the same settings agree and the comparison
never succeeds - every upload through FileUploadToResourceField was rejected.
Validate the token by decrypting it and comparing the settings it carries
instead. The authentication tag is what makes the token unforgeable, and
decryption returns null for anything tampered with, so the check keeps
rejecting a client that alters the limits.
Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
.../upload/resource/AbstractFileUploadResource.java | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/AbstractFileUploadResource.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/AbstractFileUploadResource.java
index dea29a3494..c20e7a5ce8 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/AbstractFileUploadResource.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/AbstractFileUploadResource.java
@@ -227,9 +227,21 @@ public abstract class AbstractFileUploadResource extends AbstractResource
return false;
}
- String expectedToken = createUploadToken(uploadId, maxSize, fileMaxSize, fileCountMax);
- return MessageDigest.isEqual(expectedToken.getBytes(StandardCharsets.UTF_8),
- actualToken.getBytes(StandardCharsets.UTF_8));
+ // the token is authenticated but not deterministic - encryption uses a fresh nonce per
+ // message - so it cannot be validated by re-encrypting and comparing ciphertext. Decrypt
+ // it instead and compare the settings it carries; the authentication tag is what makes it
+ // unforgeable, and decryption fails on anything that was tampered with.
+ String decryptedToken = Application.get().getSecuritySettings().getCryptFactory().newCrypt()
+ .decryptUrlSafe(actualToken);
+ if (decryptedToken == null)
+ {
+ return false;
+ }
+
+ String expectedSettings = serializeUploadSettings(uploadId, maxSize, fileMaxSize,
+ fileCountMax);
+ return MessageDigest.isEqual(expectedSettings.getBytes(StandardCharsets.UTF_8),
+ decryptedToken.getBytes(StandardCharsets.UTF_8));
}
private ResourceResponse createErrorResponse(ResourceResponse resourceResponse, String errorMessage)