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

papegaaij pushed a commit to branch resource-name-iterator-alloc
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit a9de962b215cab807128aed51aeae020eb15ee45
Author: Emond Papegaaij <[email protected]>
AuthorDate: Mon Sep 7 05:12:50 2026 +0000

    Let a crypt scheme reserve room for its caller's prefix
    
    SchemeCrypt prefixes every ciphertext with a one-byte marker naming the 
scheme
    that produced it. It did so by allocating a new array one byte longer and
    copying the whole ciphertext into it, and on the way back by copying 
everything
    after the marker out again - two full copies of a page-sized payload for the
    sake of one byte. AbstractAesGcmCryptScheme added a third, taking the
    ciphertext as an array of its own before copying it in behind the nonce.
    
    ICryptScheme's encrypt and decrypt now carry the offsets needed to avoid all
    three. encrypt leaves prefixLength bytes free at the front of the result, so
    SchemeCrypt writes its marker in place; decrypt takes an offset and a 
length,
    so SchemeCrypt hands over the buffer it already has and simply skips the
    marker. The old signatures remain as default methods delegating with 0 and 
the
    full range.
    
    The marker stays SchemeCrypt's concern, as documented in the user guide - a
    scheme still knows nothing about it, only that the first few bytes of what 
it
    returns are not its own.
    
    PageEncryptionBenchmark, 40kB payload:
    
      encrypt  18.28 -> 12.58 us/op   124,496 -> 44,416 B/op
      decrypt  14.07 -> 11.17 us/op    84,408 -> 44,360 B/op
    
    Encrypting a payload now allocates 1.11 times its size rather than 3.1 
times:
    one array and no copies, where there were three arrays and two copies.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../core/util/crypt/AbstractAesGcmCryptScheme.java | 31 +++++----
 .../wicket/core/util/crypt/ICryptScheme.java       | 80 +++++++++++++++++++++-
 .../apache/wicket/core/util/crypt/SchemeCrypt.java | 24 ++++---
 3 files changed, 110 insertions(+), 25 deletions(-)

diff --git 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractAesGcmCryptScheme.java
 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractAesGcmCryptScheme.java
index ce1df1ef28..bd653424a3 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractAesGcmCryptScheme.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/core/util/crypt/AbstractAesGcmCryptScheme.java
@@ -84,18 +84,20 @@ public abstract class AbstractAesGcmCryptScheme implements 
ICryptScheme
        }
 
        @Override
-       public byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, 
SecureRandom random)
+       public byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, 
SecureRandom random,
+               int prefixLength)
        {
                byte[] nonce = new byte[NONCE_LENGTH];
                random.nextBytes(nonce);
 
-               return encrypt(plaintext, key, aad, nonce);
+               return encrypt(plaintext, key, aad, nonce, prefixLength);
        }
 
        @Override
-       public byte[] encryptDeterministic(byte[] plaintext, SecretKey key, 
byte[] aad)
+       public byte[] encryptDeterministic(byte[] plaintext, SecretKey key, 
byte[] aad,
+               int prefixLength)
        {
-               return encrypt(plaintext, key, aad, deriveNonce(plaintext, key, 
aad));
+               return encrypt(plaintext, key, aad, deriveNonce(plaintext, key, 
aad), prefixLength);
        }
 
        /**
@@ -112,7 +114,8 @@ public abstract class AbstractAesGcmCryptScheme implements 
ICryptScheme
         *            the nonce to use, {@link #NONCE_LENGTH} bytes
         * @return {@code nonce || ciphertext || tag}
         */
-       private byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, 
byte[] nonce)
+       private byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, 
byte[] nonce,
+               int prefixLength)
        {
                try
                {
@@ -123,10 +126,14 @@ public abstract class AbstractAesGcmCryptScheme 
implements ICryptScheme
                                cipher.updateAAD(aad);
                        }
 
-                       byte[] ciphertext = cipher.doFinal(plaintext);
+                       // Let the cipher write straight into the result, 
behind the caller's prefix and the
+                       // nonce. Taking the ciphertext as an array of its own 
first means allocating the whole
+                       // payload a second time and then copying it across.
+                       byte[] result =
+                               new byte[prefixLength + nonce.length + 
cipher.getOutputSize(plaintext.length)];
+                       System.arraycopy(nonce, 0, result, prefixLength, 
nonce.length);
+                       cipher.doFinal(plaintext, 0, plaintext.length, result, 
prefixLength + nonce.length);
 
-                       byte[] result = Arrays.copyOf(nonce, nonce.length + 
ciphertext.length);
-                       System.arraycopy(ciphertext, 0, result, nonce.length, 
ciphertext.length);
                        return result;
                }
                catch (GeneralSecurityException ex)
@@ -174,16 +181,16 @@ public abstract class AbstractAesGcmCryptScheme 
implements ICryptScheme
        }
 
        @Override
-       public byte[] decrypt(byte[] ciphertext, SecretKey key, byte[] aad)
+       public byte[] decrypt(byte[] ciphertext, int offset, int length, 
SecretKey key, byte[] aad)
        {
                try
                {
-                       if (ciphertext.length < NONCE_LENGTH)
+                       if (length < NONCE_LENGTH)
                        {
                                return null;
                        }
 
-                       byte[] nonce = Arrays.copyOfRange(ciphertext, 0, 
NONCE_LENGTH);
+                       byte[] nonce = Arrays.copyOfRange(ciphertext, offset, 
offset + NONCE_LENGTH);
 
                        Cipher cipher = getCipher();
                        cipher.init(Cipher.DECRYPT_MODE, key, 
newParameterSpec(nonce));
@@ -192,7 +199,7 @@ public abstract class AbstractAesGcmCryptScheme implements 
ICryptScheme
                                cipher.updateAAD(aad);
                        }
 
-                       return cipher.doFinal(ciphertext, NONCE_LENGTH, 
ciphertext.length - NONCE_LENGTH);
+                       return cipher.doFinal(ciphertext, offset + 
NONCE_LENGTH, length - NONCE_LENGTH);
                }
                catch (GeneralSecurityException ex)
                {
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 405bf95323..8c89cdc0c7 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
@@ -78,7 +78,34 @@ public interface ICryptScheme
         *            source of randomness for the nonce
         * @return the ciphertext (including nonce and authentication tag)
         */
-       byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, 
SecureRandom random);
+       /**
+        * Encrypts, leaving {@code prefixLength} bytes untouched at the start 
of the result for the
+        * caller to fill in. {@link SchemeCrypt} puts its scheme marker there; 
without the reservation
+        * it would have to copy the whole ciphertext to make room for a single 
byte.
+        *
+        * @param plaintext
+        *            what to encrypt
+        * @param key
+        *            the key to encrypt with
+        * @param aad
+        *            additional authenticated data, may be {@code null}
+        * @param random
+        *            source of randomness for the nonce
+        * @param prefixLength
+        *            how many bytes to leave free at the start of the result
+        * @return a freshly allocated array holding the ciphertext, preceded 
by {@code prefixLength}
+        *         bytes the caller is free to write into
+        */
+       byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, 
SecureRandom random,
+               int prefixLength);
+
+       /**
+        * @see #encrypt(byte[], SecretKey, byte[], SecureRandom, int)
+        */
+       default byte[] encrypt(byte[] plaintext, SecretKey key, byte[] aad, 
SecureRandom random)
+       {
+               return encrypt(plaintext, key, aad, random, 0);
+       }
 
        /**
         * Encrypt the given plaintext deterministically: the same {@code 
plaintext}, {@code key} and
@@ -101,7 +128,30 @@ public interface ICryptScheme
         *            additional authenticated data (the scheme marker); 
authenticated but not encrypted
         * @return the ciphertext (including nonce and authentication tag)
         */
-       byte[] encryptDeterministic(byte[] plaintext, SecretKey key, byte[] 
aad);
+       /**
+        * As {@link #encrypt(byte[], SecretKey, byte[], SecureRandom, int)}, 
but deriving the nonce
+        * from the input so that the same input yields the same ciphertext.
+        *
+        * @param plaintext
+        *            what to encrypt
+        * @param key
+        *            the key to encrypt with
+        * @param aad
+        *            additional authenticated data, may be {@code null}
+        * @param prefixLength
+        *            how many bytes to leave free at the start of the result
+        * @return a freshly allocated array holding the ciphertext, preceded 
by {@code prefixLength}
+        *         bytes the caller is free to write into
+        */
+       byte[] encryptDeterministic(byte[] plaintext, SecretKey key, byte[] 
aad, int prefixLength);
+
+       /**
+        * @see #encryptDeterministic(byte[], SecretKey, byte[], int)
+        */
+       default byte[] encryptDeterministic(byte[] plaintext, SecretKey key, 
byte[] aad)
+       {
+               return encryptDeterministic(plaintext, key, aad, 0);
+       }
 
        /**
         * Decrypt the given ciphertext.
@@ -116,5 +166,29 @@ public interface ICryptScheme
         * @return the decrypted plaintext, or {@code null} if authentication 
fails or the input is
         *         malformed
         */
-       byte[] decrypt(byte[] ciphertext, SecretKey key, byte[] aad);
+       /**
+        * Decrypts {@code length} bytes of {@code ciphertext} starting at 
{@code offset}, so that a
+        * caller which prefixed the ciphertext can skip its own header without 
copying the rest.
+        *
+        * @param ciphertext
+        *            the buffer holding the ciphertext
+        * @param offset
+        *            where the ciphertext starts
+        * @param length
+        *            how many bytes of ciphertext there are
+        * @param key
+        *            the key to decrypt with
+        * @param aad
+        *            additional authenticated data, may be {@code null}
+        * @return the plaintext, or {@code null} if the input is not authentic
+        */
+       byte[] decrypt(byte[] ciphertext, int offset, int length, SecretKey 
key, byte[] aad);
+
+       /**
+        * @see #decrypt(byte[], int, int, SecretKey, byte[])
+        */
+       default byte[] decrypt(byte[] ciphertext, SecretKey key, byte[] aad)
+       {
+               return decrypt(ciphertext, 0, ciphertext.length, key, aad);
+       }
 }
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 e7fdaf6b86..35dddd6178 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
@@ -17,7 +17,6 @@
 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;
@@ -49,6 +48,9 @@ public class SchemeCrypt implements ICrypt
 
        private final SecureRandom random;
 
+       /** Every ciphertext starts with a one-byte marker naming the scheme 
that produced it. */
+       private static final int MARKER_LENGTH = 1;
+
        private final ICryptScheme encryptionScheme;
 
        private final Map<Byte, ICryptScheme> allowedSchemes;
@@ -88,7 +90,7 @@ public class SchemeCrypt implements ICrypt
 
                byte id = encryptionScheme.id();
                return marked(id,
-                       encryptionScheme.encrypt(plainBytes, key, aad(id, 
associatedData), random));
+                       encryptionScheme.encrypt(plainBytes, key, aad(id, 
associatedData), random, MARKER_LENGTH));
        }
 
        @Override
@@ -97,8 +99,8 @@ public class SchemeCrypt implements ICrypt
                Args.notNull(plainBytes, "plainBytes");
 
                byte id = encryptionScheme.id();
-               return marked(id,
-                       encryptionScheme.encryptDeterministic(plainBytes, key, 
aad(id, associatedData)));
+               return marked(id, 
encryptionScheme.encryptDeterministic(plainBytes, key,
+                       aad(id, associatedData), MARKER_LENGTH));
        }
 
        @Override
@@ -117,19 +119,21 @@ public class SchemeCrypt implements ICrypt
                        return null;
                }
 
-               byte[] payload = Arrays.copyOfRange(encryptedBytes, 1, 
encryptedBytes.length);
-               return scheme.decrypt(payload, key, aad(id, associatedData));
+               return scheme.decrypt(encryptedBytes, MARKER_LENGTH, 
encryptedBytes.length - MARKER_LENGTH,
+                       key, aad(id, associatedData));
        }
 
        /**
         * Prefixes a scheme payload with the marker identifying the scheme 
that produced it.
         */
+       /**
+        * The scheme left {@link #MARKER_LENGTH} bytes free at the front for 
exactly this, so the
+        * marker is written in place rather than by copying the whole 
ciphertext along one byte.
+        */
        private static byte[] marked(byte marker, byte[] payload)
        {
-               byte[] result = new byte[payload.length + 1];
-               result[0] = marker;
-               System.arraycopy(payload, 0, result, 1, payload.length);
-               return result;
+               payload[0] = marker;
+               return payload;
        }
 
        /**

Reply via email to