Skip to content

Commit deab5c5

Browse files
author
Jochen Burkhardt
committed
Update to bouncy castle 1.8.2 Issue #192
1 parent 557aac4 commit deab5c5

File tree

6 files changed

+209
-59
lines changed

6 files changed

+209
-59
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# HAP-Java 2.0.8
2+
* Updated bouncy castle to 1.82
3+
14
# HAP-Java 2.0.7
25
* Add overloads to characteristics so that the username can be passed through.
36

pom.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,16 @@
105105

106106
<dependency>
107107
<groupId>org.bouncycastle</groupId>
108-
<artifactId>bcprov-jdk15on</artifactId>
109-
<version>1.51</version>
108+
<artifactId>bcpkix-jdk18on</artifactId>
109+
<version>1.82</version>
110110
</dependency>
111111

112+
<dependency>
113+
<groupId>org.bouncycastle</groupId>
114+
<artifactId>bctls-jdk18on</artifactId>
115+
<version>1.82</version>
116+
</dependency>
117+
112118
<dependency>
113119
<groupId>net.vrallev.ecc</groupId>
114120
<artifactId>ecc-25519-java</artifactId>
Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,98 @@
11
package io.github.hapjava.server.impl.crypto;
22

33
import java.io.IOException;
4-
import org.bouncycastle.crypto.engines.ChaChaEngine;
5-
import org.bouncycastle.crypto.generators.Poly1305KeyGenerator;
4+
import org.bouncycastle.crypto.InvalidCipherTextException;
5+
import org.bouncycastle.crypto.modes.ChaCha20Poly1305;
6+
import org.bouncycastle.crypto.params.AEADParameters;
67
import org.bouncycastle.crypto.params.KeyParameter;
7-
import org.bouncycastle.crypto.params.ParametersWithIV;
8-
import org.bouncycastle.crypto.tls.AlertDescription;
9-
import org.bouncycastle.crypto.tls.TlsFatalAlert;
10-
import org.bouncycastle.util.Arrays;
8+
import org.bouncycastle.tls.AlertDescription;
9+
import org.bouncycastle.tls.TlsFatalAlert;
1110

1211
public class ChachaDecoder {
1312

14-
private final ChaChaEngine decryptCipher;
13+
private final ChaCha20Poly1305 cipher;
14+
private final byte[] key;
15+
private final byte[] nonce;
1516

1617
public ChachaDecoder(byte[] key, byte[] nonce) throws IOException {
18+
this.key = key;
19+
// ChaCha20-Poly1305 requires exactly 12 bytes (96 bits) for nonce
20+
this.nonce = ensureNonceSize(nonce);
21+
this.cipher = new ChaCha20Poly1305();
22+
}
1723

18-
this.decryptCipher = new ChaChaEngine(20);
24+
private byte[] ensureNonceSize(byte[] nonce) {
25+
if (nonce == null) {
26+
return new byte[12]; // Default to zero nonce if null
27+
}
1928

20-
this.decryptCipher.init(false, new ParametersWithIV(new KeyParameter(key), nonce));
29+
// For HomeKit pairing messages, handle Apple's string-based nonces
30+
if (nonce.length == 8) {
31+
// Apple's HomeKit implementation uses a specific nonce format
32+
// Based on RFC 7539 and Apple's implementation, the nonce should be:
33+
// - 4 bytes constant (0x00000000)
34+
// - 8 bytes nonce string
35+
// This matches ChaCha20's 96-bit nonce requirement
36+
byte[] adjustedNonce = new byte[12];
37+
// Put the 8-byte nonce at the END (bytes 4-11), not at the beginning
38+
System.arraycopy(nonce, 0, adjustedNonce, 4, 8);
39+
// First 4 bytes remain zero (counter initialization)
40+
return adjustedNonce;
41+
}
42+
43+
if (nonce.length == 12) {
44+
return nonce; // Already correct size
45+
}
46+
47+
// For other nonce lengths, pad or truncate to 12 bytes
48+
byte[] adjustedNonce = new byte[12];
49+
if (nonce.length < 12) {
50+
// Pad with zeros if too short - put nonce at beginning
51+
System.arraycopy(nonce, 0, adjustedNonce, 0, nonce.length);
52+
// Remaining bytes are already zero
53+
} else {
54+
// Truncate if too long - take first 12 bytes
55+
System.arraycopy(nonce, 0, adjustedNonce, 0, 12);
56+
}
57+
58+
return adjustedNonce;
2159
}
2260

2361
public byte[] decodeCiphertext(byte[] receivedMAC, byte[] additionalData, byte[] ciphertext)
2462
throws IOException {
2563

26-
KeyParameter macKey = initRecordMAC(decryptCipher);
64+
try {
65+
byte[] ciphertextWithTag = new byte[ciphertext.length + receivedMAC.length];
66+
System.arraycopy(ciphertext, 0, ciphertextWithTag, 0, ciphertext.length);
67+
System.arraycopy(receivedMAC, 0, ciphertextWithTag, ciphertext.length, receivedMAC.length);
68+
69+
ChaCha20Poly1305 cipher1 = new ChaCha20Poly1305();
70+
AEADParameters params = new AEADParameters(new KeyParameter(key), 128, nonce, additionalData);
71+
cipher1.init(false, params);
2772

28-
byte[] calculatedMAC = PolyKeyCreator.create(macKey, additionalData, ciphertext);
73+
byte[] output = new byte[cipher1.getOutputSize(ciphertextWithTag.length)];
74+
int len = cipher1.processBytes(ciphertextWithTag, 0, ciphertextWithTag.length, output, 0);
75+
len += cipher1.doFinal(output, len);
2976

30-
if (!Arrays.constantTimeAreEqual(calculatedMAC, receivedMAC)) {
77+
byte[] result = new byte[len];
78+
System.arraycopy(output, 0, result, 0, len);
79+
80+
return result;
81+
82+
} catch (InvalidCipherTextException e) {
3183
throw new TlsFatalAlert(AlertDescription.bad_record_mac);
3284
}
85+
}
3386

34-
byte[] output = new byte[ciphertext.length];
35-
decryptCipher.processBytes(ciphertext, 0, ciphertext.length, output, 0);
36-
37-
return output;
87+
private static String bytesToHex(byte[] bytes) {
88+
StringBuilder result = new StringBuilder();
89+
for (byte b : bytes) {
90+
result.append(String.format("%02x", b));
91+
}
92+
return result.toString();
3893
}
3994

4095
public byte[] decodeCiphertext(byte[] receivedMAC, byte[] ciphertext) throws IOException {
4196
return decodeCiphertext(receivedMAC, null, ciphertext);
4297
}
43-
44-
private KeyParameter initRecordMAC(ChaChaEngine cipher) {
45-
byte[] firstBlock = new byte[64];
46-
cipher.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0);
47-
48-
// NOTE: The BC implementation puts 'r' after 'k'
49-
System.arraycopy(firstBlock, 0, firstBlock, 32, 16);
50-
KeyParameter macKey = new KeyParameter(firstBlock, 16, 32);
51-
Poly1305KeyGenerator.clamp(macKey.getKey());
52-
return macKey;
53-
}
5498
}
Lines changed: 70 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,93 @@
11
package io.github.hapjava.server.impl.crypto;
22

33
import java.io.IOException;
4-
import org.bouncycastle.crypto.engines.ChaChaEngine;
5-
import org.bouncycastle.crypto.generators.Poly1305KeyGenerator;
4+
import org.bouncycastle.crypto.InvalidCipherTextException;
5+
import org.bouncycastle.crypto.modes.ChaCha20Poly1305;
6+
import org.bouncycastle.crypto.params.AEADParameters;
67
import org.bouncycastle.crypto.params.KeyParameter;
7-
import org.bouncycastle.crypto.params.ParametersWithIV;
88

99
public class ChachaEncoder {
1010

11-
private final ChaChaEngine encryptCipher;
11+
private final ChaCha20Poly1305 cipher;
12+
private final byte[] key;
13+
private final byte[] nonce;
1214

1315
public ChachaEncoder(byte[] key, byte[] nonce) throws IOException {
16+
this.key = key;
17+
// ChaCha20-Poly1305 requires exactly 12 bytes (96 bits) for nonce
18+
this.nonce = ensureNonceSize(nonce);
19+
this.cipher = new ChaCha20Poly1305();
20+
}
21+
22+
private byte[] ensureNonceSize(byte[] nonce) {
23+
if (nonce == null) {
24+
return new byte[12]; // Default to zero nonce if null
25+
}
26+
27+
// For HomeKit pairing messages, handle Apple's string-based nonces
28+
if (nonce.length == 8) {
29+
// Apple's HomeKit implementation uses a specific nonce format
30+
// Based on RFC 7539 and Apple's implementation, the nonce should be:
31+
// - 4 bytes constant (0x00000000)
32+
// - 8 bytes nonce string
33+
// This matches ChaCha20's 96-bit nonce requirement and ChachaDecoder format
34+
byte[] adjustedNonce = new byte[12];
35+
// Put the 8-byte nonce at the END (bytes 4-11), not at the beginning
36+
System.arraycopy(nonce, 0, adjustedNonce, 4, 8);
37+
// First 4 bytes remain zero (counter initialization)
38+
return adjustedNonce;
39+
}
40+
41+
if (nonce.length == 12) {
42+
return nonce; // Already correct size
43+
}
1444

15-
this.encryptCipher = new ChaChaEngine(20);
45+
byte[] adjustedNonce = new byte[12];
46+
if (nonce.length < 12) {
47+
// Pad with zeros if too short
48+
System.arraycopy(nonce, 0, adjustedNonce, 0, nonce.length);
49+
} else {
50+
// Truncate if too long
51+
System.arraycopy(nonce, 0, adjustedNonce, 0, 12);
52+
}
53+
return adjustedNonce;
54+
}
1655

17-
this.encryptCipher.init(true, new ParametersWithIV(new KeyParameter(key), nonce));
56+
private static String bytesToHex(byte[] bytes) {
57+
StringBuilder result = new StringBuilder();
58+
for (byte b : bytes) {
59+
result.append(String.format("%02x", b));
60+
}
61+
return result.toString();
1862
}
1963

2064
public byte[] encodeCiphertext(byte[] plaintext) throws IOException {
2165
return encodeCiphertext(plaintext, null);
2266
}
2367

2468
public byte[] encodeCiphertext(byte[] plaintext, byte[] additionalData) throws IOException {
25-
KeyParameter macKey = initRecordMAC(encryptCipher);
26-
27-
byte[] ciphertext = new byte[plaintext.length];
28-
encryptCipher.processBytes(plaintext, 0, plaintext.length, ciphertext, 0);
69+
try {
70+
// Use the nonce provided during construction
71+
AEADParameters params = new AEADParameters(new KeyParameter(key), 128, nonce, additionalData);
72+
cipher.init(true, params);
2973

30-
byte[] calculatedMAC = PolyKeyCreator.create(macKey, additionalData, ciphertext);
31-
32-
byte[] ret = new byte[ciphertext.length + 16];
33-
System.arraycopy(ciphertext, 0, ret, 0, ciphertext.length);
34-
System.arraycopy(calculatedMAC, 0, ret, ciphertext.length, 16);
35-
return ret;
36-
}
74+
byte[] output = new byte[cipher.getOutputSize(plaintext.length)];
75+
int len = cipher.processBytes(plaintext, 0, plaintext.length, output, 0);
76+
len += cipher.doFinal(output, len);
3777

38-
private KeyParameter initRecordMAC(ChaChaEngine cipher) {
39-
byte[] firstBlock = new byte[64];
40-
cipher.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0);
78+
// Split the result into ciphertext and MAC
79+
byte[] ciphertext = new byte[plaintext.length];
80+
byte[] mac = new byte[16];
81+
System.arraycopy(output, 0, ciphertext, 0, plaintext.length);
82+
System.arraycopy(output, plaintext.length, mac, 0, 16);
4183

42-
// NOTE: The BC implementation puts 'r' after 'k'
43-
System.arraycopy(firstBlock, 0, firstBlock, 32, 16);
44-
KeyParameter macKey = new KeyParameter(firstBlock, 16, 32);
45-
Poly1305KeyGenerator.clamp(macKey.getKey());
46-
return macKey;
84+
// Return combined ciphertext + MAC as expected by the original interface
85+
byte[] ret = new byte[ciphertext.length + 16];
86+
System.arraycopy(ciphertext, 0, ret, 0, ciphertext.length);
87+
System.arraycopy(mac, 0, ret, ciphertext.length, 16);
88+
return ret;
89+
} catch (InvalidCipherTextException e) {
90+
throw new IOException("Encryption failed", e);
91+
}
4792
}
4893
}

src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java

100644100755
Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public ExchangeHandler(byte[] k, HomekitAuthInfo authInfo) {
3131
}
3232

3333
public HttpResponse handle(PairSetupRequest req) throws Exception {
34+
LOGGER.debug("ExchangeHandler: Starting M5 exchange with shared secret K: {}", bytesToHex(k));
35+
3436
HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA512Digest());
3537
hkdf.init(
3638
new HKDFParameters(
@@ -40,12 +42,26 @@ public HttpResponse handle(PairSetupRequest req) throws Exception {
4042
byte[] okm = hkdf_enc_key = new byte[32];
4143
hkdf.generateBytes(okm, 0, 32);
4244

45+
LOGGER.debug("ExchangeHandler: HKDF encryption key: {}", bytesToHex(okm));
46+
4347
return decrypt((ExchangeRequest) req, okm);
4448
}
4549

4650
private HttpResponse decrypt(ExchangeRequest req, byte[] key) throws Exception {
47-
ChachaDecoder chacha = new ChachaDecoder(key, "PS-Msg05".getBytes(StandardCharsets.UTF_8));
48-
byte[] plaintext = chacha.decodeCiphertext(req.getAuthTagData(), req.getMessageData());
51+
LOGGER.debug("ExchangeHandler: Received AuthTag: {}", bytesToHex(req.getAuthTagData()));
52+
LOGGER.debug("ExchangeHandler: Received MessageData: {}", bytesToHex(req.getMessageData()));
53+
54+
try {
55+
ChachaDecoder chacha = new ChachaDecoder(key, "PS-Msg05".getBytes(StandardCharsets.UTF_8));
56+
byte[] plaintext = chacha.decodeCiphertext(req.getAuthTagData(), req.getMessageData());
57+
return processDecryptedData(plaintext);
58+
} catch (Exception e) {
59+
LOGGER.error("ExchangeHandler: M5 decryption failed: {}", e.getMessage());
60+
throw new RuntimeException("HomeKit M5 message decryption failed", e);
61+
}
62+
}
63+
64+
private HttpResponse processDecryptedData(byte[] plaintext) throws Exception {
4965

5066
DecodeResult d = TypeLengthValueUtils.decode(plaintext);
5167
byte[] username = d.getBytes(MessageType.USERNAME);
@@ -54,6 +70,14 @@ private HttpResponse decrypt(ExchangeRequest req, byte[] key) throws Exception {
5470
return createUser(username, ltpk, proof);
5571
}
5672

73+
private static String bytesToHex(byte[] bytes) {
74+
StringBuilder result = new StringBuilder();
75+
for (byte b : bytes) {
76+
result.append(String.format("%02x", b));
77+
}
78+
return result.toString();
79+
}
80+
5781
private HttpResponse createUser(byte[] username, byte[] ltpk, byte[] proof) throws Exception {
5882
HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA512Digest());
5983
hkdf.init(

src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java

100644100755
Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,38 @@ static class ExchangeRequest extends PairSetupRequest {
8585
private final byte[] authTagData;
8686

8787
public ExchangeRequest(DecodeResult d) {
88-
messageData = new byte[d.getLength(MessageType.ENCRYPTED_DATA) - 16];
88+
// Get the complete encrypted data field
89+
byte[] encryptedData = d.getBytes(MessageType.ENCRYPTED_DATA);
90+
logger.debug("ExchangeRequest: Total encrypted data length: {}", encryptedData.length);
91+
logger.debug("ExchangeRequest: Raw encrypted data: {}", bytesToHex(encryptedData));
92+
93+
// For HomeKit M5, the encrypted data contains ciphertext + 16-byte auth tag
94+
// The auth tag is the LAST 16 bytes
95+
if (encryptedData.length < 16) {
96+
throw new RuntimeException(
97+
"Encrypted data too short, expected at least 16 bytes for auth tag");
98+
}
99+
100+
int ciphertextLength = encryptedData.length - 16;
101+
messageData = new byte[ciphertextLength];
89102
authTagData = new byte[16];
90-
d.getBytes(MessageType.ENCRYPTED_DATA, messageData, 0);
91-
d.getBytes(MessageType.ENCRYPTED_DATA, authTagData, messageData.length);
103+
104+
// Copy ciphertext (everything except last 16 bytes)
105+
System.arraycopy(encryptedData, 0, messageData, 0, ciphertextLength);
106+
// Copy auth tag (last 16 bytes)
107+
System.arraycopy(encryptedData, ciphertextLength, authTagData, 0, 16);
108+
109+
logger.debug("ExchangeRequest: Parsed ciphertext length: {}", messageData.length);
110+
logger.debug("ExchangeRequest: Parsed ciphertext: {}", bytesToHex(messageData));
111+
logger.debug("ExchangeRequest: Parsed auth tag: {}", bytesToHex(authTagData));
112+
}
113+
114+
private static String bytesToHex(byte[] bytes) {
115+
StringBuilder result = new StringBuilder();
116+
for (byte b : bytes) {
117+
result.append(String.format("%02x", b));
118+
}
119+
return result.toString();
92120
}
93121

94122
public byte[] getMessageData() {

0 commit comments

Comments
 (0)