Skip to content

Commit c345a4b

Browse files
author
Eugen Freiter
committed
add support for qr code
Signed-off-by: Eugen Freiter <freiter@gmx.de>
1 parent 031450c commit c345a4b

File tree

5 files changed

+76
-4
lines changed

5 files changed

+76
-4
lines changed

src/main/java/io/github/hapjava/server/HomekitAuthInfo.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ public interface HomekitAuthInfo {
2121
*/
2222
String getPin();
2323

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

src/main/java/io/github/hapjava/server/impl/HomekitRoot.java

Lines changed: 2 additions & 1 deletion
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
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
9+
private static byte[] calculateHash(final String input, final Digest digest) {
10+
byte[] inputAsBytes = input.getBytes();
11+
byte[] retValue = new byte[digest.getDigestSize()];
12+
digest.update(inputAsBytes, 0, inputAsBytes.length);
13+
digest.doFinal(retValue, 0);
14+
return retValue;
15+
}
16+
17+
/**
18+
* generate SHA52 Hash for given string. The hash is used for mDNS advertisement.
19+
*
20+
* @param value value
21+
* @return hash
22+
*/
23+
public static String generateSHA512Hash(final String value) {
24+
final byte[] hash = calculateHash(value.toUpperCase(), new SHA512Digest());
25+
final byte[] hashTuncate = new byte[4];
26+
System.arraycopy(hash, 0, hashTuncate, 0, 4);
27+
String hashStr = Base64.getEncoder().encodeToString(hashTuncate);
28+
return hashStr;
29+
}
30+
31+
/**
32+
* generate Setup URI which can be used fo QR Code generation.
33+
*
34+
* @param pin PIN number without "-"
35+
* @param setupId alphanumeric string of the length 4
36+
* @param category accessory category
37+
* @return setup UID
38+
*/
39+
public static String getSetupURI(final String pin, final String setupId, final int category) {
40+
long code =
41+
0 << 43 // Version
42+
| 0 << 39 // Reserved
43+
| ((long) category) << 31 // Category
44+
| 0 << 29 // BLE support
45+
| 1 << 28 // IP support
46+
| 0 << 27 // Paired / NFC
47+
| Integer.valueOf(pin); // PIN
48+
String payload = Long.toString(code, 36) + setupId;
49+
while (payload.length() < 13) {
50+
payload = '0' + payload;
51+
}
52+
return "X-HM://" + payload.toUpperCase();
53+
}
54+
}

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

Lines changed: 7 additions & 2 deletions
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);
@@ -84,6 +88,7 @@ private void registerService() throws IOException {
8488
props.put("sf", discoverable ? "1" : "0");
8589
props.put("id", mac);
8690
props.put("md", label);
91+
props.put("sh", generateSHA512Hash(setupId + mac));
8792
props.put("c#", Integer.toString(configurationIndex));
8893
props.put("s#", "1");
8994
props.put("ff", "0");

src/test/java/io/github/hapjava/server/impl/HomekitRootTest.java

Lines changed: 5 additions & 1 deletion
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)