diff --git a/CHANGES.md b/CHANGES.md
index a7e700425..a04801320 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,6 @@
+# HAP-Java 2.0.8
+* Updated bouncy castle to 1.82
+
# HAP-Java 2.0.7
* Add overloads to characteristics so that the username can be passed through.
diff --git a/pom.xml b/pom.xml
index ffd5798a3..e533e7d01 100644
--- a/pom.xml
+++ b/pom.xml
@@ -105,10 +105,16 @@
org.bouncycastle
- bcprov-jdk15on
- 1.51
+ bcpkix-jdk18on
+ 1.82
+
+ org.bouncycastle
+ bctls-jdk18on
+ 1.82
+
+
net.vrallev.ecc
ecc-25519-java
diff --git a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java
old mode 100644
new mode 100755
index 4c74a469d..014f4b500
--- a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java
+++ b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaDecoder.java
@@ -1,54 +1,98 @@
package io.github.hapjava.server.impl.crypto;
import java.io.IOException;
-import org.bouncycastle.crypto.engines.ChaChaEngine;
-import org.bouncycastle.crypto.generators.Poly1305KeyGenerator;
+import org.bouncycastle.crypto.InvalidCipherTextException;
+import org.bouncycastle.crypto.modes.ChaCha20Poly1305;
+import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
-import org.bouncycastle.crypto.params.ParametersWithIV;
-import org.bouncycastle.crypto.tls.AlertDescription;
-import org.bouncycastle.crypto.tls.TlsFatalAlert;
-import org.bouncycastle.util.Arrays;
+import org.bouncycastle.tls.AlertDescription;
+import org.bouncycastle.tls.TlsFatalAlert;
public class ChachaDecoder {
- private final ChaChaEngine decryptCipher;
+ private final ChaCha20Poly1305 cipher;
+ private final byte[] key;
+ private final byte[] nonce;
public ChachaDecoder(byte[] key, byte[] nonce) throws IOException {
+ this.key = key;
+ // ChaCha20-Poly1305 requires exactly 12 bytes (96 bits) for nonce
+ this.nonce = ensureNonceSize(nonce);
+ this.cipher = new ChaCha20Poly1305();
+ }
- this.decryptCipher = new ChaChaEngine(20);
+ private byte[] ensureNonceSize(byte[] nonce) {
+ if (nonce == null) {
+ return new byte[12]; // Default to zero nonce if null
+ }
- this.decryptCipher.init(false, new ParametersWithIV(new KeyParameter(key), nonce));
+ // For HomeKit pairing messages, handle Apple's string-based nonces
+ if (nonce.length == 8) {
+ // Apple's HomeKit implementation uses a specific nonce format
+ // Based on RFC 7539 and Apple's implementation, the nonce should be:
+ // - 4 bytes constant (0x00000000)
+ // - 8 bytes nonce string
+ // This matches ChaCha20's 96-bit nonce requirement
+ byte[] adjustedNonce = new byte[12];
+ // Put the 8-byte nonce at the END (bytes 4-11), not at the beginning
+ System.arraycopy(nonce, 0, adjustedNonce, 4, 8);
+ // First 4 bytes remain zero (counter initialization)
+ return adjustedNonce;
+ }
+
+ if (nonce.length == 12) {
+ return nonce; // Already correct size
+ }
+
+ // For other nonce lengths, pad or truncate to 12 bytes
+ byte[] adjustedNonce = new byte[12];
+ if (nonce.length < 12) {
+ // Pad with zeros if too short - put nonce at beginning
+ System.arraycopy(nonce, 0, adjustedNonce, 0, nonce.length);
+ // Remaining bytes are already zero
+ } else {
+ // Truncate if too long - take first 12 bytes
+ System.arraycopy(nonce, 0, adjustedNonce, 0, 12);
+ }
+
+ return adjustedNonce;
}
public byte[] decodeCiphertext(byte[] receivedMAC, byte[] additionalData, byte[] ciphertext)
throws IOException {
- KeyParameter macKey = initRecordMAC(decryptCipher);
+ try {
+ byte[] ciphertextWithTag = new byte[ciphertext.length + receivedMAC.length];
+ System.arraycopy(ciphertext, 0, ciphertextWithTag, 0, ciphertext.length);
+ System.arraycopy(receivedMAC, 0, ciphertextWithTag, ciphertext.length, receivedMAC.length);
+
+ ChaCha20Poly1305 cipher1 = new ChaCha20Poly1305();
+ AEADParameters params = new AEADParameters(new KeyParameter(key), 128, nonce, additionalData);
+ cipher1.init(false, params);
- byte[] calculatedMAC = PolyKeyCreator.create(macKey, additionalData, ciphertext);
+ byte[] output = new byte[cipher1.getOutputSize(ciphertextWithTag.length)];
+ int len = cipher1.processBytes(ciphertextWithTag, 0, ciphertextWithTag.length, output, 0);
+ len += cipher1.doFinal(output, len);
- if (!Arrays.constantTimeAreEqual(calculatedMAC, receivedMAC)) {
+ byte[] result = new byte[len];
+ System.arraycopy(output, 0, result, 0, len);
+
+ return result;
+
+ } catch (InvalidCipherTextException e) {
throw new TlsFatalAlert(AlertDescription.bad_record_mac);
}
+ }
- byte[] output = new byte[ciphertext.length];
- decryptCipher.processBytes(ciphertext, 0, ciphertext.length, output, 0);
-
- return output;
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder result = new StringBuilder();
+ for (byte b : bytes) {
+ result.append(String.format("%02x", b));
+ }
+ return result.toString();
}
public byte[] decodeCiphertext(byte[] receivedMAC, byte[] ciphertext) throws IOException {
return decodeCiphertext(receivedMAC, null, ciphertext);
}
-
- private KeyParameter initRecordMAC(ChaChaEngine cipher) {
- byte[] firstBlock = new byte[64];
- cipher.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0);
-
- // NOTE: The BC implementation puts 'r' after 'k'
- System.arraycopy(firstBlock, 0, firstBlock, 32, 16);
- KeyParameter macKey = new KeyParameter(firstBlock, 16, 32);
- Poly1305KeyGenerator.clamp(macKey.getKey());
- return macKey;
- }
}
diff --git a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java
old mode 100644
new mode 100755
index 3304e7d9a..889c9ca86
--- a/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java
+++ b/src/main/java/io/github/hapjava/server/impl/crypto/ChachaEncoder.java
@@ -1,20 +1,64 @@
package io.github.hapjava.server.impl.crypto;
import java.io.IOException;
-import org.bouncycastle.crypto.engines.ChaChaEngine;
-import org.bouncycastle.crypto.generators.Poly1305KeyGenerator;
+import org.bouncycastle.crypto.InvalidCipherTextException;
+import org.bouncycastle.crypto.modes.ChaCha20Poly1305;
+import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
-import org.bouncycastle.crypto.params.ParametersWithIV;
public class ChachaEncoder {
- private final ChaChaEngine encryptCipher;
+ private final ChaCha20Poly1305 cipher;
+ private final byte[] key;
+ private final byte[] nonce;
public ChachaEncoder(byte[] key, byte[] nonce) throws IOException {
+ this.key = key;
+ // ChaCha20-Poly1305 requires exactly 12 bytes (96 bits) for nonce
+ this.nonce = ensureNonceSize(nonce);
+ this.cipher = new ChaCha20Poly1305();
+ }
+
+ private byte[] ensureNonceSize(byte[] nonce) {
+ if (nonce == null) {
+ return new byte[12]; // Default to zero nonce if null
+ }
+
+ // For HomeKit pairing messages, handle Apple's string-based nonces
+ if (nonce.length == 8) {
+ // Apple's HomeKit implementation uses a specific nonce format
+ // Based on RFC 7539 and Apple's implementation, the nonce should be:
+ // - 4 bytes constant (0x00000000)
+ // - 8 bytes nonce string
+ // This matches ChaCha20's 96-bit nonce requirement and ChachaDecoder format
+ byte[] adjustedNonce = new byte[12];
+ // Put the 8-byte nonce at the END (bytes 4-11), not at the beginning
+ System.arraycopy(nonce, 0, adjustedNonce, 4, 8);
+ // First 4 bytes remain zero (counter initialization)
+ return adjustedNonce;
+ }
+
+ if (nonce.length == 12) {
+ return nonce; // Already correct size
+ }
- this.encryptCipher = new ChaChaEngine(20);
+ byte[] adjustedNonce = new byte[12];
+ if (nonce.length < 12) {
+ // Pad with zeros if too short
+ System.arraycopy(nonce, 0, adjustedNonce, 0, nonce.length);
+ } else {
+ // Truncate if too long
+ System.arraycopy(nonce, 0, adjustedNonce, 0, 12);
+ }
+ return adjustedNonce;
+ }
- this.encryptCipher.init(true, new ParametersWithIV(new KeyParameter(key), nonce));
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder result = new StringBuilder();
+ for (byte b : bytes) {
+ result.append(String.format("%02x", b));
+ }
+ return result.toString();
}
public byte[] encodeCiphertext(byte[] plaintext) throws IOException {
@@ -22,27 +66,28 @@ public byte[] encodeCiphertext(byte[] plaintext) throws IOException {
}
public byte[] encodeCiphertext(byte[] plaintext, byte[] additionalData) throws IOException {
- KeyParameter macKey = initRecordMAC(encryptCipher);
-
- byte[] ciphertext = new byte[plaintext.length];
- encryptCipher.processBytes(plaintext, 0, plaintext.length, ciphertext, 0);
+ try {
+ // Use the nonce provided during construction
+ AEADParameters params = new AEADParameters(new KeyParameter(key), 128, nonce, additionalData);
+ cipher.init(true, params);
- byte[] calculatedMAC = PolyKeyCreator.create(macKey, additionalData, ciphertext);
-
- byte[] ret = new byte[ciphertext.length + 16];
- System.arraycopy(ciphertext, 0, ret, 0, ciphertext.length);
- System.arraycopy(calculatedMAC, 0, ret, ciphertext.length, 16);
- return ret;
- }
+ byte[] output = new byte[cipher.getOutputSize(plaintext.length)];
+ int len = cipher.processBytes(plaintext, 0, plaintext.length, output, 0);
+ len += cipher.doFinal(output, len);
- private KeyParameter initRecordMAC(ChaChaEngine cipher) {
- byte[] firstBlock = new byte[64];
- cipher.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0);
+ // Split the result into ciphertext and MAC
+ byte[] ciphertext = new byte[plaintext.length];
+ byte[] mac = new byte[16];
+ System.arraycopy(output, 0, ciphertext, 0, plaintext.length);
+ System.arraycopy(output, plaintext.length, mac, 0, 16);
- // NOTE: The BC implementation puts 'r' after 'k'
- System.arraycopy(firstBlock, 0, firstBlock, 32, 16);
- KeyParameter macKey = new KeyParameter(firstBlock, 16, 32);
- Poly1305KeyGenerator.clamp(macKey.getKey());
- return macKey;
+ // Return combined ciphertext + MAC as expected by the original interface
+ byte[] ret = new byte[ciphertext.length + 16];
+ System.arraycopy(ciphertext, 0, ret, 0, ciphertext.length);
+ System.arraycopy(mac, 0, ret, ciphertext.length, 16);
+ return ret;
+ } catch (InvalidCipherTextException e) {
+ throw new IOException("Encryption failed", e);
+ }
}
}
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
old mode 100644
new mode 100755
index 2310975fa..4b7c3eb11
--- 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,6 +31,8 @@ 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(
@@ -40,12 +42,26 @@ 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 {
- ChachaDecoder chacha = new ChachaDecoder(key, "PS-Msg05".getBytes(StandardCharsets.UTF_8));
- byte[] plaintext = chacha.decodeCiphertext(req.getAuthTagData(), req.getMessageData());
+ 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());
+ return processDecryptedData(plaintext);
+ } catch (Exception e) {
+ LOGGER.error("ExchangeHandler: M5 decryption failed: {}", e.getMessage());
+ throw new RuntimeException("HomeKit M5 message decryption failed", e);
+ }
+ }
+
+ private HttpResponse processDecryptedData(byte[] plaintext) throws Exception {
DecodeResult d = TypeLengthValueUtils.decode(plaintext);
byte[] username = d.getBytes(MessageType.USERNAME);
@@ -54,6 +70,14 @@ private HttpResponse decrypt(ExchangeRequest req, byte[] key) throws Exception {
return createUser(username, ltpk, proof);
}
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder result = new StringBuilder();
+ for (byte b : bytes) {
+ result.append(String.format("%02x", b));
+ }
+ return result.toString();
+ }
+
private HttpResponse createUser(byte[] username, byte[] ltpk, byte[] proof) throws Exception {
HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA512Digest());
hkdf.init(
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java
old mode 100644
new mode 100755
index 4e7260fed..6fa099be4
--- a/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java
@@ -85,10 +85,38 @@ static class ExchangeRequest extends PairSetupRequest {
private final byte[] authTagData;
public ExchangeRequest(DecodeResult d) {
- messageData = new byte[d.getLength(MessageType.ENCRYPTED_DATA) - 16];
+ // Get the complete encrypted data field
+ byte[] encryptedData = d.getBytes(MessageType.ENCRYPTED_DATA);
+ logger.debug("ExchangeRequest: Total encrypted data length: {}", encryptedData.length);
+ logger.debug("ExchangeRequest: Raw encrypted data: {}", bytesToHex(encryptedData));
+
+ // For HomeKit M5, the encrypted data contains ciphertext + 16-byte auth tag
+ // The auth tag is the LAST 16 bytes
+ if (encryptedData.length < 16) {
+ throw new RuntimeException(
+ "Encrypted data too short, expected at least 16 bytes for auth tag");
+ }
+
+ int ciphertextLength = encryptedData.length - 16;
+ messageData = new byte[ciphertextLength];
authTagData = new byte[16];
- d.getBytes(MessageType.ENCRYPTED_DATA, messageData, 0);
- d.getBytes(MessageType.ENCRYPTED_DATA, authTagData, messageData.length);
+
+ // Copy ciphertext (everything except last 16 bytes)
+ System.arraycopy(encryptedData, 0, messageData, 0, ciphertextLength);
+ // Copy auth tag (last 16 bytes)
+ System.arraycopy(encryptedData, ciphertextLength, authTagData, 0, 16);
+
+ logger.debug("ExchangeRequest: Parsed ciphertext length: {}", messageData.length);
+ logger.debug("ExchangeRequest: Parsed ciphertext: {}", bytesToHex(messageData));
+ logger.debug("ExchangeRequest: Parsed auth tag: {}", bytesToHex(authTagData));
+ }
+
+ private static String bytesToHex(byte[] bytes) {
+ StringBuilder result = new StringBuilder();
+ for (byte b : bytes) {
+ result.append(String.format("%02x", b));
+ }
+ return result.toString();
}
public byte[] getMessageData() {