Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/main/java/io/github/hapjava/server/HomekitAuthInfo.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.hapjava.server;

import io.github.hapjava.server.impl.HomekitServer;
import io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils;
import java.math.BigInteger;

/**
Expand All @@ -21,6 +22,16 @@ public interface HomekitAuthInfo {
*/
String getPin();

/**
* A setup Id used for pairing the device using QR Code. It can be any alphanumeric combination of
* the length of 4.
*
* @return setup id
*/
default String getSetupId() {
return HAPSetupCodeUtils.generateSetupId();
}

/**
* A unique MAC address to be advertised with the HomeKit information. This does not have to be
* the MAC address of the network interface. You can generate this using {@link
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/io/github/hapjava/server/impl/HomekitRoot.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ public void start() {
port -> {
try {
refreshAuthInfo();
advertiser.advertise(label, authInfo.getMac(), port, configurationIndex);
advertiser.advertise(
label, authInfo.getMac(), port, configurationIndex, authInfo.getSetupId());
} catch (Exception e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.github.hapjava.server.impl.crypto;

import java.util.Base64;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA512Digest;

public class HAPSetupCodeUtils {
private static final String ALPHA_NUMERIC_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

public static String randomAlphaNumeric(int count) {
StringBuilder builder = new StringBuilder();
while (count-- != 0) {
int character = (int) (Math.random() * ALPHA_NUMERIC_STRING.length());
builder.append(ALPHA_NUMERIC_STRING.charAt(character));
}
return builder.toString();
}

public static String generateSetupId() {
return randomAlphaNumeric(4);
}

private static byte[] calculateHash(final String input, final Digest digest) {
byte[] inputAsBytes = input.getBytes();
byte[] retValue = new byte[digest.getDigestSize()];
digest.update(inputAsBytes, 0, inputAsBytes.length);
digest.doFinal(retValue, 0);
return retValue;
}

/**
* generate SHA52 Hash for given string. The hash is used for mDNS advertisement.
*
* @param value value
* @return hash
*/
public static String generateSHA512Hash(final String value) {
final byte[] hash = calculateHash(value.toUpperCase(), new SHA512Digest());
final byte[] hashTuncate = new byte[4];
System.arraycopy(hash, 0, hashTuncate, 0, 4);
String hashStr = Base64.getEncoder().encodeToString(hashTuncate);
return hashStr;
}

/**
* generate Setup URI which can be used fo QR Code generation.
*
* @param pin PIN number without "-"
* @param setupId alphanumeric string of the length 4
* @param category accessory category
* @return setup UID
*/
public static String getSetupURI(final String pin, final String setupId, final int category) {
long code =
0 << 43 // Version
| 0 << 39 // Reserved
| ((long) category) << 31 // Category
| 0 << 29 // BLE support
| 1 << 28 // IP support
| 0 << 27 // Paired / NFC
| Integer.valueOf(pin); // PIN
String payload = Long.toString(code, 36) + setupId;
while (payload.length() < 13) {
payload = '0' + payload;
}
return "X-HM://" + payload.toUpperCase();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.github.hapjava.server.impl.jmdns;

import static io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils.generateSHA512Hash;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
Expand All @@ -21,21 +23,23 @@ public class JmdnsHomekitAdvertiser {

private String label;
private String mac;
private String setupId;
private int port;
private int configurationIndex;

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

public synchronized void advertise(String label, String mac, int port, int configurationIndex)
throws Exception {
public synchronized void advertise(
String label, String mac, int port, int configurationIndex, String setupId) throws Exception {
if (isAdvertising) {
throw new IllegalStateException("HomeKit advertiser is already running");
}
this.label = label;
this.mac = mac;
this.port = port;
this.setupId = setupId;
this.configurationIndex = configurationIndex;

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

private void registerService() throws IOException {
logger.info("Registering " + SERVICE_TYPE + " on port " + port);
logger.trace("MAC:" + mac + " Setup Id:" + setupId);
Map<String, String> props = new HashMap<>();
props.put("sf", discoverable ? "1" : "0");
props.put("id", mac);
props.put("md", label);
props.put("sh", generateSHA512Hash(setupId + mac));
props.put("c#", Integer.toString(configurationIndex));
props.put("s#", "1");
props.put("ff", "0");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class HomekitRootTest {
private HomekitAuthInfo authInfo;

private static final int PORT = 12345;
private static final String SETUPID = "Gx12";

private static final String LABEL = "Test Label";

@Before
Expand Down Expand Up @@ -73,8 +75,10 @@ public void testWebHandlerStops() throws Exception {
public void testAdvertiserStarts() throws Exception {
final String mac = "00:00:00:00:00:00";
when(authInfo.getMac()).thenReturn(mac);
when(authInfo.getSetupId()).thenReturn(SETUPID);

root.start();
verify(advertiser).advertise(eq(LABEL), eq(mac), eq(PORT), eq(1));
verify(advertiser).advertise(eq(LABEL), eq(mac), eq(PORT), eq(1), eq(SETUPID));
}

@Test
Expand Down