From d6d4dce3150c23bbe8d0246f0b8b3f73f7633dee Mon Sep 17 00:00:00 2001 From: udaw Date: Sun, 18 Oct 2026 22:31:43 +0200 Subject: [PATCH 1/2] Do not log pairing secrets in ExchangeHandler Remove DEBUG log statements that printed the SRP shared secret K, the HKDF-derived encryption key and the raw M5 auth tag / message data. No logic changes. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 1 + .../hapjava/server/impl/pairing/ExchangeHandler.java | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4997cae9..147cdd37 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ # HAP-Java 2.0.9 * Fix Set Duration and Remaining Duration characteristics to advertise format `uint32` instead of `int` [#202](https://github.com/hap-java/HAP-Java/issues/202) +* Stop logging the SRP shared secret, derived key and M5 message data during pairing at DEBUG level # HAP-Java 2.0.8 * Updated bouncy castle to 1.83 diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java index 4b7c3eb1..6bae3ab8 100755 --- a/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java @@ -31,8 +31,6 @@ public ExchangeHandler(byte[] k, HomekitAuthInfo authInfo) { } public HttpResponse handle(PairSetupRequest req) throws Exception { - LOGGER.debug("ExchangeHandler: Starting M5 exchange with shared secret K: {}", bytesToHex(k)); - HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA512Digest()); hkdf.init( new HKDFParameters( @@ -42,15 +40,10 @@ public HttpResponse handle(PairSetupRequest req) throws Exception { byte[] okm = hkdf_enc_key = new byte[32]; hkdf.generateBytes(okm, 0, 32); - LOGGER.debug("ExchangeHandler: HKDF encryption key: {}", bytesToHex(okm)); - return decrypt((ExchangeRequest) req, okm); } private HttpResponse decrypt(ExchangeRequest req, byte[] key) throws Exception { - LOGGER.debug("ExchangeHandler: Received AuthTag: {}", bytesToHex(req.getAuthTagData())); - LOGGER.debug("ExchangeHandler: Received MessageData: {}", bytesToHex(req.getMessageData())); - try { ChachaDecoder chacha = new ChachaDecoder(key, "PS-Msg05".getBytes(StandardCharsets.UTF_8)); byte[] plaintext = chacha.decodeCiphertext(req.getAuthTagData(), req.getMessageData()); From bf5955412a023304ffc0f8ffcd198538092329d8 Mon Sep 17 00:00:00 2001 From: udaw Date: Sun, 18 Oct 2026 22:32:47 +0200 Subject: [PATCH 2/2] Fix frame splitting in LengthPrefixedByteArrayProcessor - Read the length of each frame at the current position instead of always at the start of the read, so several frames of different length in one read are split correctly. - Clear the stored first length byte once it has been used, and do not take the fast path while such a byte is pending, so a read that ends after the first length byte is handled by step(). Adds tests that lock in the existing behaviour (single frame, several frames in one read, a frame split across two reads) plus regression tests for the two cases above. Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 1 + .../LengthPrefixedByteArrayProcessor.java | 5 +- .../LengthPrefixedByteArrayProcessorTest.java | 125 ++++++++++++++++++ 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 src/test/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessorTest.java diff --git a/CHANGES.md b/CHANGES.md index 147cdd37..a50fc7fa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ # HAP-Java 2.0.9 * Fix Set Duration and Remaining Duration characteristics to advertise format `uint32` instead of `int` [#202](https://github.com/hap-java/HAP-Java/issues/202) * Stop logging the SRP shared secret, derived key and M5 message data during pairing at DEBUG level +* Fix splitting of encrypted frames when several frames of different length arrive in one read, or when a read ends after the first length byte # HAP-Java 2.0.8 * Updated bouncy castle to 1.83 diff --git a/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java b/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java index 124acbb5..34ec3a41 100644 --- a/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java +++ b/src/main/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessor.java @@ -21,9 +21,9 @@ public synchronized Collection handle(byte[] data) { int pos = 0; LOGGER.trace( "Received message of length {}. Existing buffer is {}", data.length, buffer.size()); - if (buffer.size() == 0) { + if (buffer.size() == 0 && firstLengthByteBuffer == null) { while (data.length - pos > 18) { - int targetLength = (data[0] & 0xFF) + (data[1] & 0xFF) * 256 + 16 + 2; + int targetLength = (data[pos] & 0xFF) + (data[pos + 1] & 0xFF) * 256 + 16 + 2; LOGGER.trace("Attempting to read message of length {}", targetLength); if (data.length >= pos + targetLength) { byte[] b = new byte[targetLength - 2]; @@ -56,6 +56,7 @@ private void step(byte[] data, int pos, Collection results) { if (targetLength == 0) { if (firstLengthByteBuffer != null) { targetLength = (firstLengthByteBuffer & 0xFF) + (data[pos] & 0xFF) * 256 + 16; + firstLengthByteBuffer = null; pos += 1; LOGGER.trace( "Received the second byte after storing the first byte. New length is {}", diff --git a/src/test/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessorTest.java b/src/test/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessorTest.java new file mode 100644 index 00000000..9e839848 --- /dev/null +++ b/src/test/java/io/github/hapjava/server/impl/connections/LengthPrefixedByteArrayProcessorTest.java @@ -0,0 +1,125 @@ +package io.github.hapjava.server.impl.connections; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.junit.Test; + +public class LengthPrefixedByteArrayProcessorTest { + + private static final int TAG_LENGTH = 16; + + /** + * Builds a frame as it arrives on the wire: 2-byte little-endian payload length, payload, 16-byte + * tag. The payload and tag bytes start at 0x80 so that they never look like a short length. + */ + private static byte[] frame(int payloadLength, int seed) { + byte[] frame = new byte[2 + payloadLength + TAG_LENGTH]; + frame[0] = (byte) (payloadLength & 0xFF); + frame[1] = (byte) ((payloadLength >> 8) & 0xFF); + for (int i = 2; i < frame.length; i++) { + frame[i] = (byte) (0x80 + seed + i); + } + return frame; + } + + /** What the processor is expected to emit for a frame: payload + tag, without the length. */ + private static byte[] body(byte[] frame) { + return Arrays.copyOfRange(frame, 2, frame.length); + } + + private static byte[] concat(byte[]... parts) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (byte[] part : parts) { + out.write(part, 0, part.length); + } + return out.toByteArray(); + } + + private static List feed(LengthPrefixedByteArrayProcessor processor, byte[]... reads) { + List results = new ArrayList<>(); + for (byte[] read : reads) { + results.addAll(processor.handle(read)); + } + return results; + } + + private static List sizes(Collection results) { + List sizes = new ArrayList<>(); + for (byte[] result : results) { + sizes.add(result.length); + } + return sizes; + } + + private static void assertBodies(List results, byte[]... frames) { + List expectedSizes = new ArrayList<>(); + for (byte[] frame : frames) { + expectedSizes.add(frame.length - 2); + } + assertThat(sizes(results)).isEqualTo(expectedSizes); + for (int i = 0; i < frames.length; i++) { + assertThat(results.get(i)).as("frame %d", i).isEqualTo(body(frames[i])); + } + } + + @Test + public void singleFrameInOneRead() { + byte[] a = frame(100, 1); + assertBodies(feed(new LengthPrefixedByteArrayProcessor(), a), a); + } + + @Test + public void threeFramesInOneRead() { + byte[] a = frame(1024, 1); + byte[] b = frame(1024, 2); + byte[] c = frame(200, 3); + assertBodies(feed(new LengthPrefixedByteArrayProcessor(), concat(a, b, c)), a, b, c); + } + + @Test + public void frameSplitAcrossTwoReads() { + byte[] a = frame(1024, 1); + int[] offsets = {1, 2, 3, 10, 18, 19, 20, 500, a.length - 1}; + for (int offset : offsets) { + byte[] first = Arrays.copyOfRange(a, 0, offset); + byte[] second = Arrays.copyOfRange(a, offset, a.length); + List results = feed(new LengthPrefixedByteArrayProcessor(), first, second); + assertThat(sizes(results)).as("split at %d", offset).containsExactly(a.length - 2); + assertThat(results.get(0)).as("split at %d", offset).isEqualTo(body(a)); + } + } + + @Test + public void frameSplitWithNextFrameInSecondRead() { + byte[] a = frame(1024, 1); + byte[] b = frame(200, 2); + byte[] first = Arrays.copyOfRange(a, 0, 500); + byte[] second = concat(Arrays.copyOfRange(a, 500, a.length), b); + assertBodies(feed(new LengthPrefixedByteArrayProcessor(), first, second), a, b); + } + + @Test + public void shortFrameFollowedByLongerFrameInOneRead() { + byte[] a = frame(5, 1); + byte[] b = frame(100, 2); + List results = feed(new LengthPrefixedByteArrayProcessor(), concat(a, b)); + assertThat(sizes(results)).containsExactly(21, 116); + assertBodies(results, a, b); + } + + @Test + public void singleLengthByteThenRestThenAnotherFrame() { + byte[] a = frame(5, 1); + byte[] b = frame(40, 2); + byte[] first = Arrays.copyOfRange(a, 0, 1); + byte[] second = concat(Arrays.copyOfRange(a, 1, a.length), b); + List results = feed(new LengthPrefixedByteArrayProcessor(), first, second); + assertThat(sizes(results)).containsExactly(21, 56); + assertBodies(results, a, b); + } +}