Skip to content

Commit 8232818

Browse files
authored
Merge pull request #121 from yfre/qrcode
add support for QR Code for pairing
2 parents a746a02 + c80f3e7 commit 8232818

File tree

5 files changed

+94
-4
lines changed

5 files changed

+94
-4
lines changed

Diff for: src/main/java/io/github/hapjava/server/HomekitAuthInfo.java

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.hapjava.server;
22

33
import io.github.hapjava.server.impl.HomekitServer;
4+
import io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils;
45
import java.math.BigInteger;
56

67
/**
@@ -21,6 +22,16 @@ public interface HomekitAuthInfo {
2122
*/
2223
String getPin();
2324

25+
/**
26+
* A setup Id used for pairing the device using QR Code. It can be any alphanumeric combination of
27+
* the length of 4.
28+
*
29+
* @return setup id
30+
*/
31+
default String getSetupId() {
32+
return HAPSetupCodeUtils.generateSetupId();
33+
}
34+
2435
/**
2536
* A unique MAC address to be advertised with the HomeKit information. This does not have to be
2637
* the MAC address of the network interface. You can generate this using {@link

Diff for: src/main/java/io/github/hapjava/server/impl/HomekitRoot.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ public void start() {
116116
port -> {
117117
try {
118118
refreshAuthInfo();
119-
advertiser.advertise(label, authInfo.getMac(), port, configurationIndex);
119+
advertiser.advertise(
120+
label, authInfo.getMac(), port, configurationIndex, authInfo.getSetupId());
120121
} catch (Exception e) {
121122
throw new RuntimeException(e);
122123
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.github.hapjava.server.impl.crypto;
2+
3+
import java.util.Base64;
4+
import org.bouncycastle.crypto.Digest;
5+
import org.bouncycastle.crypto.digests.SHA512Digest;
6+
7+
public class HAPSetupCodeUtils {
8+
private static final String ALPHA_NUMERIC_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
9+
10+
public static String randomAlphaNumeric(int count) {
11+
StringBuilder builder = new StringBuilder();
12+
while (count-- != 0) {
13+
int character = (int) (Math.random() * ALPHA_NUMERIC_STRING.length());
14+
builder.append(ALPHA_NUMERIC_STRING.charAt(character));
15+
}
16+
return builder.toString();
17+
}
18+
19+
public static String generateSetupId() {
20+
return randomAlphaNumeric(4);
21+
}
22+
23+
private static byte[] calculateHash(final String input, final Digest digest) {
24+
byte[] inputAsBytes = input.getBytes();
25+
byte[] retValue = new byte[digest.getDigestSize()];
26+
digest.update(inputAsBytes, 0, inputAsBytes.length);
27+
digest.doFinal(retValue, 0);
28+
return retValue;
29+
}
30+
31+
/**
32+
* generate SHA52 Hash for given string. The hash is used for mDNS advertisement.
33+
*
34+
* @param value value
35+
* @return hash
36+
*/
37+
public static String generateSHA512Hash(final String value) {
38+
final byte[] hash = calculateHash(value.toUpperCase(), new SHA512Digest());
39+
final byte[] hashTuncate = new byte[4];
40+
System.arraycopy(hash, 0, hashTuncate, 0, 4);
41+
String hashStr = Base64.getEncoder().encodeToString(hashTuncate);
42+
return hashStr;
43+
}
44+
45+
/**
46+
* generate Setup URI which can be used fo QR Code generation.
47+
*
48+
* @param pin PIN number without "-"
49+
* @param setupId alphanumeric string of the length 4
50+
* @param category accessory category
51+
* @return setup UID
52+
*/
53+
public static String getSetupURI(final String pin, final String setupId, final int category) {
54+
long code =
55+
0 << 43 // Version
56+
| 0 << 39 // Reserved
57+
| ((long) category) << 31 // Category
58+
| 0 << 29 // BLE support
59+
| 1 << 28 // IP support
60+
| 0 << 27 // Paired / NFC
61+
| Integer.valueOf(pin); // PIN
62+
String payload = Long.toString(code, 36) + setupId;
63+
while (payload.length() < 13) {
64+
payload = '0' + payload;
65+
}
66+
return "X-HM://" + payload.toUpperCase();
67+
}
68+
}

Diff for: src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.github.hapjava.server.impl.jmdns;
22

3+
import static io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils.generateSHA512Hash;
4+
35
import java.io.IOException;
46
import java.net.InetAddress;
57
import java.net.UnknownHostException;
@@ -21,21 +23,23 @@ public class JmdnsHomekitAdvertiser {
2123

2224
private String label;
2325
private String mac;
26+
private String setupId;
2427
private int port;
2528
private int configurationIndex;
2629

2730
public JmdnsHomekitAdvertiser(InetAddress localAddress) throws UnknownHostException, IOException {
2831
jmdns = JmDNS.create(localAddress);
2932
}
3033

31-
public synchronized void advertise(String label, String mac, int port, int configurationIndex)
32-
throws Exception {
34+
public synchronized void advertise(
35+
String label, String mac, int port, int configurationIndex, String setupId) throws Exception {
3336
if (isAdvertising) {
3437
throw new IllegalStateException("HomeKit advertiser is already running");
3538
}
3639
this.label = label;
3740
this.mac = mac;
3841
this.port = port;
42+
this.setupId = setupId;
3943
this.configurationIndex = configurationIndex;
4044

4145
logger.trace("Advertising accessory " + label);
@@ -80,10 +84,12 @@ public synchronized void setConfigurationIndex(int revision) throws IOException
8084

8185
private void registerService() throws IOException {
8286
logger.info("Registering " + SERVICE_TYPE + " on port " + port);
87+
logger.trace("MAC:" + mac + " Setup Id:" + setupId);
8388
Map<String, String> props = new HashMap<>();
8489
props.put("sf", discoverable ? "1" : "0");
8590
props.put("id", mac);
8691
props.put("md", label);
92+
props.put("sh", generateSHA512Hash(setupId + mac));
8793
props.put("c#", Integer.toString(configurationIndex));
8894
props.put("s#", "1");
8995
props.put("ff", "0");

Diff for: src/test/java/io/github/hapjava/server/impl/HomekitRootTest.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public class HomekitRootTest {
2626
private HomekitAuthInfo authInfo;
2727

2828
private static final int PORT = 12345;
29+
private static final String SETUPID = "Gx12";
30+
2931
private static final String LABEL = "Test Label";
3032

3133
@Before
@@ -73,8 +75,10 @@ public void testWebHandlerStops() throws Exception {
7375
public void testAdvertiserStarts() throws Exception {
7476
final String mac = "00:00:00:00:00:00";
7577
when(authInfo.getMac()).thenReturn(mac);
78+
when(authInfo.getSetupId()).thenReturn(SETUPID);
79+
7680
root.start();
77-
verify(advertiser).advertise(eq(LABEL), eq(mac), eq(PORT), eq(1));
81+
verify(advertiser).advertise(eq(LABEL), eq(mac), eq(PORT), eq(1), eq(SETUPID));
7882
}
7983

8084
@Test

0 commit comments

Comments
 (0)