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));
+ }
+ }
}