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

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-codec.git


The following commit(s) were added to refs/heads/master by this push:
     new eec585d1 Fix Blake3 KDF example and clarify finalization semantics
eec585d1 is described below

commit eec585d1e8215c36b48951b901772837adac4183
Author: Gary Gregory <[email protected]>
AuthorDate: Thu Sep 17 15:50:00 2026 -0700

    Fix Blake3 KDF example and clarify finalization semantics
    
    Derive directional keys by splitting a single output. Document that
    finalization is idempotent and subsequent updates append input.
    Add regression tests for finalization, reset, and key derivation.
---
 src/changes/changes.xml                            |  3 +-
 .../org/apache/commons/codec/digest/Blake3.java    | 45 ++++++++---
 .../apache/commons/codec/digest/Blake3Test.java    | 89 ++++++++++++++++++++++
 3 files changed, 126 insertions(+), 11 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index ae10848b..b7a31c31 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -52,7 +52,8 @@ The <action> type attribute can be add,update,fix,remove.
       <action type="fix" dev="ggregory" due-to="Yu Bao, Gary Gregory">Allocate 
a single MessageDigest and use it in Sha2Crypt.sha2Crypt(byte[], String, 
String, int, String).</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Javadoc 
improvements.</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Throw 
IOException instead of IllegalArgumentException in BaseNCodecOutputStream and 
BaseNCodecOutputStream IO methods.</action>
-      <action type="fix" dev="ggregory" due-to="Gary Gregory">Throw 
DecoderException instead of IllegalArgumentException in 
RFC1522Codec.decodeText(String)..</action>
+      <action type="fix" dev="ggregory" due-to="Gary Gregory">Throw 
DecoderException instead of IllegalArgumentException in 
RFC1522Codec.decodeText(String).</action>
+      <action type="fix" dev="ggregory" due-to="Gary Gregory">Fix Blake3 KDF 
example and clarify finalization semantics.</action>
       <!-- ADD -->
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add and use 
PhoneticEngine.Builder and deprecate old constructors.</action>
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add 
BeiderMorseEncoder.Builder and deprecate old constructor.</action>
diff --git a/src/main/java/org/apache/commons/codec/digest/Blake3.java 
b/src/main/java/org/apache/commons/codec/digest/Blake3.java
index 967fe3d5..05b56c19 100644
--- a/src/main/java/org/apache/commons/codec/digest/Blake3.java
+++ b/src/main/java/org/apache/commons/codec/digest/Blake3.java
@@ -50,7 +50,9 @@ import java.util.Objects;
  * <h2>Key Derivation</h2>
  * <p>A specific hash mode for deriving session keys and other derived keys in 
a unique key derivation context
  * identified by some sequence of bytes. These context strings should be 
unique but do not need to be kept secret.
- * Additional input data is hashed for key material which can be finalized to 
derive subkeys.</p>
+ * Additional input data is hashed for key material which can be finalized to 
derive subkeys. To derive multiple subkeys,
+ * request their combined length in one finalization and split the output. 
Repeated finalizations start at the beginning
+ * of the same output and do not derive new subkeys.</p>
  * <pre>{@code
  *      String context = "org.apache.commons.codec.digest.Blake3Example";
  *      byte[] sharedSecret = ...;
@@ -60,10 +62,9 @@ import java.util.Objects;
  *      kdf.update(sharedSecret);
  *      kdf.update(senderId);
  *      kdf.update(recipientId);
- *      byte[] txKey = new byte[32];
- *      byte[] rxKey = new byte[32];
- *      kdf.doFinalize(txKey);
- *      kdf.doFinalize(rxKey);
+ *      byte[] keys = kdf.doFinalize(64);
+ *      byte[] txKey = Arrays.copyOfRange(keys, 0, 32);
+ *      byte[] rxKey = Arrays.copyOfRange(keys, 32, 64);
  * }</pre>
  * <p>
  * Adapted from the ISC-licensed O(1) Cryptography library by Matt Sicker and 
ported from the reference public domain
@@ -451,8 +452,13 @@ public final class Blake3 {
     }
 
     /**
-     * Finalizes hash output data that depends on the sequence of updated 
bytes preceding this invocation and any
-     * previously finalized bytes. Note that this can finalize up to 
2<sup>64</sup> bytes per instance.
+     * Finalizes hash output into the provided array.
+     *
+     * <p>
+     * This method does not change the hash state. Each invocation starts at 
the beginning of the output for the bytes supplied to {@code update()}.
+     * Repeated invocations without additional input produce the same output 
prefix. Additional calls to {@code update()} append input to the existing hash
+     * state, even after finalization.
+     * </p>
      *
      * @param out destination array to finalize bytes into.
      * @return {@code this} instance.
@@ -463,8 +469,13 @@ public final class Blake3 {
     }
 
     /**
-     * Finalizes an arbitrary number of bytes into the provided output array 
that depends on the sequence of previously
-     * updated and finalized bytes. Note that this can finalize up to 
2<sup>64</sup> bytes per instance.
+     * Finalizes hash output into a region of the provided array.
+     *
+     * <p>
+     * This method does not change the hash state. Each invocation starts at 
the beginning of the output for the bytes supplied to {@code update()}.
+     * Repeated invocations without additional input produce the same output 
prefix. The offset selects the destination array position, not a position in
+     * the hash output. Additional calls to {@code update()} append input to 
the existing hash state, even after finalization.
+     * </p>
      *
      * @param out    destination array to finalize bytes into.
      * @param offset where in the array to begin writing bytes to.
@@ -481,7 +492,13 @@ public final class Blake3 {
     }
 
     /**
-     * Squeezes and returns an arbitrary number of bytes dependent on the 
sequence of previously absorbed and squeezed bytes.
+     * Finalizes hash output into a new array.
+     *
+     * <p>
+     * This method does not change the hash state. Each invocation starts at 
the beginning of the output for the bytes supplied to {@code update()}.
+     * Repeated invocations without additional input produce the same output 
prefix. Additional calls to {@code update()} append input to the existing hash
+     * state, even after finalization.
+     * </p>
      *
      * @param nrBytes number of bytes to finalize.
      * @return requested number of finalized bytes.
@@ -509,6 +526,10 @@ public final class Blake3 {
     /**
      * Updates this hash state using the provided bytes.
      *
+     * <p>
+     * Input is appended to the existing hash state, including after 
finalization. Call {@link #reset()} first to start a new message.
+     * </p>
+     *
      * @param in source array to update data from.
      * @return {@code this} instance.
      * @throws NullPointerException if in is null.
@@ -520,6 +541,10 @@ public final class Blake3 {
     /**
      * Updates this hash state using the provided bytes at an offset.
      *
+     * <p>
+     * Input is appended to the existing hash state, including after 
finalization. Call {@link #reset()} first to start a new message.
+     * </p>
+     *
      * @param in     source array to update data from.
      * @param offset where in the array to begin reading bytes.
      * @param length number of bytes to update.
diff --git a/src/test/java/org/apache/commons/codec/digest/Blake3Test.java 
b/src/test/java/org/apache/commons/codec/digest/Blake3Test.java
index f0dcb2d5..9498d9e4 100644
--- a/src/test/java/org/apache/commons/codec/digest/Blake3Test.java
+++ b/src/test/java/org/apache/commons/codec/digest/Blake3Test.java
@@ -16,15 +16,88 @@
  */
 package org.apache.commons.codec.digest;
 
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 
 class Blake3Test {
     private static void assertThrowsProperExceptionWithKeySize(final int 
keySize) {
         assertThrows(IllegalArgumentException.class, () -> 
Blake3.initKeyedHash(new byte[keySize]), "Blake3 keys must be 32 bytes");
     }
 
+    private static byte[] input(final int length) {
+        final byte[] input = new byte[length];
+        for (int i = 0; i < length; i++) {
+            input[i] = (byte) (i % 251);
+        }
+        return input;
+    }
+
+    private static Blake3[] newHashers() {
+        return new Blake3[] {
+            Blake3.initHash(),
+            Blake3.initKeyedHash("whats the Elvish word for 
friend".getBytes(StandardCharsets.UTF_8)),
+            
Blake3.initKeyDerivationFunction("org.apache.commons.codec.digest.Blake3Test".getBytes(StandardCharsets.UTF_8))
+        };
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = {0, 1, 63, 64, 65, 1023, 1024, 1025, 2048, 3073})
+    void testFinalizeIsIdempotent(final int inputLength) {
+        for (final Blake3 hasher : newHashers()) {
+            hasher.update(input(inputLength));
+            final byte[] expected = hasher.doFinalize(131);
+            assertArrayEquals(expected, hasher.doFinalize(131));
+            assertArrayEquals(Arrays.copyOf(expected, 32), 
hasher.doFinalize(32));
+            assertArrayEquals(new byte[0], hasher.doFinalize(0));
+            final byte[] actual = new byte[131];
+            hasher.doFinalize(actual);
+            assertArrayEquals(expected, actual);
+            final byte[] destination = new byte[137];
+            Arrays.fill(destination, (byte) 0x5a);
+            final byte[] expectedDestination = destination.clone();
+            System.arraycopy(expected, 0, expectedDestination, 3, 
expected.length);
+            hasher.doFinalize(destination, 3, expected.length);
+            assertArrayEquals(expectedDestination, destination);
+            assertArrayEquals(expected, hasher.doFinalize(131));
+        }
+    }
+
+    @Test
+    void testKdfExampleDerivesDistinctKeys() {
+        final Blake3 kdf = 
Blake3.initKeyDerivationFunction("org.apache.commons.codec.digest.Blake3Example".getBytes(StandardCharsets.UTF_8));
+        kdf.update("shared secret".getBytes(StandardCharsets.UTF_8));
+        kdf.update("sender".getBytes(StandardCharsets.UTF_8));
+        kdf.update("recipient".getBytes(StandardCharsets.UTF_8));
+        final byte[] keys = kdf.doFinalize(64);
+        final byte[] txKey = Arrays.copyOfRange(keys, 0, 32);
+        final byte[] rxKey = Arrays.copyOfRange(keys, 32, 64);
+        assertFalse(Arrays.equals(txKey, rxKey));
+        assertArrayEquals(txKey, kdf.doFinalize(32));
+        assertArrayEquals(txKey, kdf.doFinalize(32));
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = {0, 1, 63, 64, 65, 1023, 1024, 1025, 2048, 3073})
+    void testResetAfterFinalize(final int inputLength) {
+        final Blake3[] hashers = newHashers();
+        final Blake3[] freshHashers = newHashers();
+        final byte[] message = input(inputLength);
+        for (int i = 0; i < hashers.length; i++) {
+            final Blake3 hasher = hashers[i];
+            final byte[] expected = hasher.update(message).doFinalize(131);
+            assertArrayEquals(freshHashers[i].doFinalize(131), 
hasher.reset().doFinalize(131));
+            assertArrayEquals(expected, 
hasher.update(message).doFinalize(131));
+        }
+    }
+
     @Test
     void testShouldThrowIllegalArgumentExceptionWhenIncorrectKeySize() {
         for (int i = 0; i < 32; i++) {
@@ -32,4 +105,20 @@ class Blake3Test {
         }
         assertThrowsProperExceptionWithKeySize(33);
     }
+
+    @ParameterizedTest
+    @ValueSource(ints = {0, 1, 63, 64, 65, 1023, 1024, 1025, 2048, 3073})
+    void testUpdateAfterFinalize(final int inputLength) {
+        final byte[] message = input(inputLength + 1025);
+        final Blake3[] hashers = newHashers();
+        final Blake3[] freshHashers = newHashers();
+        for (int i = 0; i < hashers.length; i++) {
+            final Blake3 hasher = hashers[i];
+            hasher.update(message, 0, inputLength).doFinalize(131);
+            hasher.update(message, inputLength, message.length - inputLength);
+            final byte[] expected = 
freshHashers[i].update(message).doFinalize(131);
+            assertArrayEquals(expected, hasher.doFinalize(131));
+            assertArrayEquals(expected, hasher.doFinalize(131));
+        }
+    }
 }

Reply via email to