diff --git a/CHANGES.md b/CHANGES.md
index 67f610bed..402d3fcec 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,6 @@
+# HAP-Java 2.0.5
+* Implement List-Pairings method. Compatibility with new Home infrastructure from iOS 16.2?
+
# HAP-Java 2.0.3
* Avoid unnecessary forced disconnects. Library users should be updating the configuration index anyway.
diff --git a/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java b/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java
index 069bf7270..6441962d7 100644
--- a/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java
+++ b/src/main/java/io/github/hapjava/server/HomekitAuthInfo.java
@@ -3,6 +3,8 @@
import io.github.hapjava.server.impl.HomekitServer;
import io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils;
import java.math.BigInteger;
+import java.util.Collection;
+import java.util.List;
/**
* Authentication info that must be provided when constructing a new {@link HomekitServer}. You will
@@ -65,8 +67,23 @@ default String getSetupId() {
* @param username the iOS device's username. The value will not be meaningful to anything but
* iOS.
* @param publicKey the iOS device's public key.
+ * @param isAdmin if the user is an admin, authorized to and/remove other users
*/
- void createUser(String username, byte[] publicKey);
+ default void createUser(String username, byte[] publicKey, boolean isAdmin) {
+ createUser(username, publicKey);
+ }
+
+ /**
+ * Deprecated method to add a user, assuming all users are admins.
+ *
+ *
At least one of the createUser methods must be implemented.
+ *
+ * @param username the iOS device's username.
+ * @param publicKey the iOS device's public key.
+ */
+ default void createUser(String username, byte[] publicKey) {
+ createUser(username, publicKey, true);
+ }
/**
* Called when an iOS device needs to remove an existing pairing. Subsequent calls to {@link
@@ -76,6 +93,15 @@ default String getSetupId() {
*/
void removeUser(String username);
+ /**
+ * List all users which have been authenticated.
+ *
+ * @return the previously stored list of users.
+ */
+ default Collection listUsers() {
+ return List.of();
+ }
+
/**
* Called when an already paired iOS device is re-connecting. The public key returned by this
* method will be compared with the signature of the pair verification request to validate the
@@ -86,6 +112,16 @@ default String getSetupId() {
*/
byte[] getUserPublicKey(String username);
+ /**
+ * Determine if the specified user is an admin.
+ *
+ * @param username the username of the iOS device to retrieve permissions for.
+ * @return the previously stored permissions.
+ */
+ default boolean userIsAdmin(String username) {
+ return true;
+ }
+
/**
* Called to check if a user has been created. The homekit accessory advertises whether the
* accessory has already been paired. At this time, it's unclear whether multiple users can be
diff --git a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java
index 1750a8d3e..36bfeefa2 100644
--- a/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java
+++ b/src/main/java/io/github/hapjava/server/impl/HomekitRoot.java
@@ -197,6 +197,7 @@ public void stop() {
* @throws IOException if there is an error in the underlying protocol, such as a TCP error
*/
public void refreshAuthInfo() throws IOException {
+ advertiser.setMac(authInfo.getMac());
advertiser.setDiscoverable(!authInfo.hasUser());
}
diff --git a/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java b/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java
index 36ac04085..f1c97c62c 100644
--- a/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java
+++ b/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java
@@ -9,9 +9,9 @@
import io.github.hapjava.server.impl.jmdns.JmdnsHomekitAdvertiser;
import io.github.hapjava.server.impl.json.AccessoryController;
import io.github.hapjava.server.impl.json.CharacteristicsController;
-import io.github.hapjava.server.impl.pairing.PairVerificationManager;
-import io.github.hapjava.server.impl.pairing.PairingManager;
-import io.github.hapjava.server.impl.pairing.PairingUpdateController;
+import io.github.hapjava.server.impl.pairing.PairSetupManager;
+import io.github.hapjava.server.impl.pairing.PairVerifyManager;
+import io.github.hapjava.server.impl.pairing.PairingsManager;
import io.github.hapjava.server.impl.responses.InternalServerErrorResponse;
import io.github.hapjava.server.impl.responses.NotFoundResponse;
import java.io.IOException;
@@ -21,8 +21,8 @@
class HttpSession {
- private volatile PairingManager pairingManager;
- private volatile PairVerificationManager pairVerificationManager;
+ private volatile PairSetupManager pairSetupManager;
+ private volatile PairVerifyManager pairVerifyManager;
private volatile AccessoryController accessoryController;
private volatile CharacteristicsController characteristicsController;
@@ -67,7 +67,7 @@ public HttpResponse handleRequest(HttpRequest request) throws IOException {
public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOException {
advertiser.setDiscoverable(
- false); // brigde is already bound and should not be discoverable anymore
+ false); // bridge is already bound and should not be discoverable anymore
try {
switch (request.getUri()) {
case "/accessories":
@@ -84,7 +84,7 @@ public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOExc
}
case "/pairings":
- return new PairingUpdateController(authInfo, advertiser).handle(request);
+ return new PairingsManager(authInfo, advertiser).handle(request);
default:
if (request.getUri().startsWith("/characteristics?")) {
@@ -100,15 +100,15 @@ public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOExc
}
private HttpResponse handlePairSetup(HttpRequest request) {
- if (pairingManager == null) {
+ if (pairSetupManager == null) {
synchronized (HttpSession.class) {
- if (pairingManager == null) {
- pairingManager = new PairingManager(authInfo, registry);
+ if (pairSetupManager == null) {
+ pairSetupManager = new PairSetupManager(authInfo, registry);
}
}
}
try {
- return pairingManager.handle(request);
+ return pairSetupManager.handle(request);
} catch (Exception e) {
logger.warn("Exception encountered during pairing", e);
return new InternalServerErrorResponse(e);
@@ -116,15 +116,15 @@ private HttpResponse handlePairSetup(HttpRequest request) {
}
private HttpResponse handlePairVerify(HttpRequest request) {
- if (pairVerificationManager == null) {
+ if (pairVerifyManager == null) {
synchronized (HttpSession.class) {
- if (pairVerificationManager == null) {
- pairVerificationManager = new PairVerificationManager(authInfo, registry);
+ if (pairVerifyManager == null) {
+ pairVerifyManager = new PairVerifyManager(authInfo, registry);
}
}
}
try {
- return pairVerificationManager.handle(request);
+ return pairVerifyManager.handle(request);
} catch (Exception e) {
logger.warn("Exception encountered while verifying pairing", e);
return new InternalServerErrorResponse(e);
diff --git a/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java b/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java
index 1535a490a..9086acb3e 100644
--- a/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java
+++ b/src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java
@@ -79,6 +79,17 @@ public synchronized void setDiscoverable(boolean discoverable) throws IOExceptio
}
}
+ public synchronized void setMac(String mac) throws IOException {
+ if (this.mac != mac) {
+ this.mac = mac;
+ if (isAdvertising) {
+ logger.trace("Re-creating service due to change in mac to " + mac);
+ unregisterService();
+ registerService();
+ }
+ }
+ }
+
public synchronized void setConfigurationIndex(int revision) throws IOException {
if (this.configurationIndex != revision) {
this.configurationIndex = revision;
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/ErrorCode.java b/src/main/java/io/github/hapjava/server/impl/pairing/ErrorCode.java
new file mode 100644
index 000000000..90112fc46
--- /dev/null
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/ErrorCode.java
@@ -0,0 +1,26 @@
+package io.github.hapjava.server.impl.pairing;
+
+public enum ErrorCode {
+ OK(0),
+ UNKNOWN(1),
+ AUTHENTICATION(2),
+ BACKOFF(3),
+ MAX_PEERS(4),
+ MAX_TRIES(5),
+ UNAVAILABLE(6),
+ BUSY(7);
+
+ private final short key;
+
+ ErrorCode(short key) {
+ this.key = key;
+ }
+
+ ErrorCode(int key) {
+ this.key = (short) key;
+ }
+
+ public short getKey() {
+ return key;
+ }
+}
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java
similarity index 84%
rename from src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java
rename to src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java
index e5d9e371c..2310975fa 100644
--- a/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java
@@ -6,22 +6,26 @@
import io.github.hapjava.server.impl.crypto.EdsaSigner;
import io.github.hapjava.server.impl.crypto.EdsaVerifier;
import io.github.hapjava.server.impl.http.HttpResponse;
-import io.github.hapjava.server.impl.pairing.PairSetupRequest.Stage3Request;
+import io.github.hapjava.server.impl.pairing.PairSetupRequest.ExchangeRequest;
import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult;
import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.Encoder;
import java.nio.charset.StandardCharsets;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
import org.bouncycastle.crypto.params.HKDFParameters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-class FinalPairHandler {
+class ExchangeHandler {
private final byte[] k;
private final HomekitAuthInfo authInfo;
private byte[] hkdf_enc_key;
- public FinalPairHandler(byte[] k, HomekitAuthInfo authInfo) {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ExchangeHandler.class);
+
+ public ExchangeHandler(byte[] k, HomekitAuthInfo authInfo) {
this.k = k;
this.authInfo = authInfo;
}
@@ -36,10 +40,10 @@ public HttpResponse handle(PairSetupRequest req) throws Exception {
byte[] okm = hkdf_enc_key = new byte[32];
hkdf.generateBytes(okm, 0, 32);
- return decrypt((Stage3Request) req, okm);
+ return decrypt((ExchangeRequest) req, okm);
}
- private HttpResponse decrypt(Stage3Request req, byte[] key) throws Exception {
+ 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());
@@ -63,9 +67,11 @@ private HttpResponse createUser(byte[] username, byte[] ltpk, byte[] proof) thro
byte[] completeData = ByteUtils.joinBytes(okm, username, ltpk);
if (!new EdsaVerifier(ltpk).verify(completeData, proof)) {
- throw new Exception("Invalid signature");
+ return new PairingResponse(6, ErrorCode.AUTHENTICATION);
}
- authInfo.createUser(authInfo.getMac() + new String(username, StandardCharsets.UTF_8), ltpk);
+ String stringUsername = new String(username, StandardCharsets.UTF_8);
+ LOGGER.trace("Creating initial user {}", stringUsername);
+ authInfo.createUser(authInfo.getMac() + stringUsername, ltpk, true);
return createResponse();
}
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/MessageType.java b/src/main/java/io/github/hapjava/server/impl/pairing/MessageType.java
index ffdd0ba33..84991d130 100644
--- a/src/main/java/io/github/hapjava/server/impl/pairing/MessageType.java
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/MessageType.java
@@ -9,7 +9,12 @@ public enum MessageType {
ENCRYPTED_DATA(5),
STATE(6),
ERROR(7),
- SIGNATURE(10);
+ SIGNATURE(0x0a),
+ PERMISSIONS(0x0b),
+ FRAGMENT_DATA(0x0c),
+ FRAGMENT_LAST(0x0d),
+ FLAGS(0x13),
+ SEPARATOR(0xff);
private final short key;
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairingManager.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupManager.java
similarity index 64%
rename from src/main/java/io/github/hapjava/server/impl/pairing/PairingManager.java
rename to src/main/java/io/github/hapjava/server/impl/pairing/PairSetupManager.java
index af6a6e01f..05b37b8ab 100644
--- a/src/main/java/io/github/hapjava/server/impl/pairing/PairingManager.java
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupManager.java
@@ -9,48 +9,49 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public class PairingManager {
+public class PairSetupManager {
- private static final Logger logger = LoggerFactory.getLogger(PairingManager.class);
+ private static final Logger logger = LoggerFactory.getLogger(PairSetupManager.class);
private final HomekitAuthInfo authInfo;
private final HomekitRegistry registry;
private SrpHandler srpHandler;
- public PairingManager(HomekitAuthInfo authInfo, HomekitRegistry registry) {
+ public PairSetupManager(HomekitAuthInfo authInfo, HomekitRegistry registry) {
this.authInfo = authInfo;
this.registry = registry;
}
public HttpResponse handle(HttpRequest httpRequest) throws Exception {
PairSetupRequest req = PairSetupRequest.of(httpRequest.getBody());
+ logger.trace("Handling pair-setup request {}", req);
- if (req.getStage() == Stage.ONE) {
- logger.trace("Starting pair for " + registry.getLabel());
+ if (req.getState() == 1) {
+ logger.trace("Received SRP Start Request " + registry.getLabel());
srpHandler = new SrpHandler(authInfo.getPin(), authInfo.getSalt());
return srpHandler.handle(req);
- } else if (req.getStage() == Stage.TWO) {
- logger.trace("Entering second stage of pair for " + registry.getLabel());
+ } else if (req.getState() == 3) {
+ logger.trace("Receive SRP Verify Request for " + registry.getLabel());
if (srpHandler == null) {
- logger.warn("Received unexpected stage 2 request for " + registry.getLabel());
+ logger.warn("Received unexpected SRP Verify Request for " + registry.getLabel());
return new UnauthorizedResponse();
} else {
try {
return srpHandler.handle(req);
} catch (Exception e) {
srpHandler = null; // You don't get to try again - need a new key
- logger.warn("Exception encountered while processing pairing request", e);
+ logger.warn("Exception encountered while processing SRP Verify Request", e);
return new UnauthorizedResponse();
}
}
- } else if (req.getStage() == Stage.THREE) {
- logger.trace("Entering third stage of pair for " + registry.getLabel());
+ } else if (req.getState() == 5) {
+ logger.trace("Received Exchange Request for " + registry.getLabel());
if (srpHandler == null) {
- logger.warn("Received unexpected stage 3 request for " + registry.getLabel());
+ logger.warn("Received unexpected Exchanged Request for " + registry.getLabel());
return new UnauthorizedResponse();
} else {
- FinalPairHandler handler = new FinalPairHandler(srpHandler.getK(), authInfo);
+ ExchangeHandler handler = new ExchangeHandler(srpHandler.getK(), authInfo);
try {
return handler.handle(req);
} catch (Exception e) {
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
index bd0da4c81..4e7260fed 100644
--- a/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairSetupRequest.java
@@ -2,46 +2,61 @@
import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult;
import java.math.BigInteger;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
abstract class PairSetupRequest {
-
- private static final short VALUE_STAGE_1 = 1;
- private static final short VALUE_STAGE_2 = 3;
- private static final short VALUE_STAGE_3 = 5;
+ private static final Logger logger = LoggerFactory.getLogger(PairSetupRequest.class);
public static PairSetupRequest of(byte[] content) throws Exception {
DecodeResult d = TypeLengthValueUtils.decode(content);
+ logger.trace("Decoded pair setup request: {}", d);
short stage = d.getByte(MessageType.STATE);
switch (stage) {
- case VALUE_STAGE_1:
- return new Stage1Request();
+ case 1:
+ return new SRPStartRequest(d);
- case VALUE_STAGE_2:
- return new Stage2Request(d);
+ case 3:
+ return new SRPVerifyRequest(d);
- case VALUE_STAGE_3:
- return new Stage3Request(d);
+ case 5:
+ return new ExchangeRequest(d);
default:
throw new Exception("Unknown pair process stage: " + stage);
}
}
- public abstract Stage getStage();
+ // Raw integer.
+ // State of the pairing process. 1=M1, 2=M2, etc.
+ public abstract int getState();
+
+ public static class SRPStartRequest extends PairSetupRequest {
+ int flags;
+
+ public SRPStartRequest(DecodeResult d) {
+ flags = 0;
+ if (d.hasMessage(MessageType.FLAGS)) {
+ flags = d.getInt(MessageType.FLAGS);
+ }
+ }
- public static class Stage1Request extends PairSetupRequest {
@Override
- public Stage getStage() {
- return Stage.ONE;
+ public int getState() {
+ return 1;
+ }
+
+ public String toString() {
+ return "";
}
}
- public static class Stage2Request extends PairSetupRequest {
+ public static class SRPVerifyRequest extends PairSetupRequest {
private final BigInteger a;
private final BigInteger m1;
- public Stage2Request(DecodeResult d) {
+ public SRPVerifyRequest(DecodeResult d) {
a = d.getBigInt(MessageType.PUBLIC_KEY);
m1 = d.getBigInt(MessageType.PROOF);
}
@@ -55,17 +70,21 @@ public BigInteger getM1() {
}
@Override
- public Stage getStage() {
- return Stage.TWO;
+ public int getState() {
+ return 3;
+ }
+
+ public String toString() {
+ return "";
}
}
- static class Stage3Request extends PairSetupRequest {
+ static class ExchangeRequest extends PairSetupRequest {
private final byte[] messageData;
private final byte[] authTagData;
- public Stage3Request(DecodeResult d) {
+ public ExchangeRequest(DecodeResult d) {
messageData = new byte[d.getLength(MessageType.ENCRYPTED_DATA) - 16];
authTagData = new byte[16];
d.getBytes(MessageType.ENCRYPTED_DATA, messageData, 0);
@@ -81,8 +100,12 @@ public byte[] getAuthTagData() {
}
@Override
- public Stage getStage() {
- return Stage.THREE;
+ public int getState() {
+ return 5;
+ }
+
+ public String toString() {
+ return "";
}
}
}
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationManager.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyManager.java
similarity index 82%
rename from src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationManager.java
rename to src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyManager.java
index d43b2d210..44f25c6d1 100644
--- a/src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationManager.java
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyManager.java
@@ -9,12 +9,11 @@
import io.github.hapjava.server.impl.crypto.EdsaVerifier;
import io.github.hapjava.server.impl.http.HttpRequest;
import io.github.hapjava.server.impl.http.HttpResponse;
-import io.github.hapjava.server.impl.pairing.PairVerificationRequest.Stage1Request;
-import io.github.hapjava.server.impl.pairing.PairVerificationRequest.Stage2Request;
+import io.github.hapjava.server.impl.pairing.PairVerifyRequest.VerifyFinishRequest;
+import io.github.hapjava.server.impl.pairing.PairVerifyRequest.VerifyStartRequest;
import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult;
import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.Encoder;
import io.github.hapjava.server.impl.responses.NotFoundResponse;
-import io.github.hapjava.server.impl.responses.OkResponse;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import org.bouncycastle.crypto.digests.SHA512Digest;
@@ -23,9 +22,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public class PairVerificationManager {
+public class PairVerifyManager {
- private static final Logger logger = LoggerFactory.getLogger(PairVerificationManager.class);
+ private static final Logger logger = LoggerFactory.getLogger(PairVerifyManager.class);
private static volatile SecureRandom secureRandom;
private final HomekitAuthInfo authInfo;
@@ -36,26 +35,26 @@ public class PairVerificationManager {
private byte[] publicKey;
private byte[] sharedSecret;
- public PairVerificationManager(HomekitAuthInfo authInfo, HomekitRegistry registry) {
+ public PairVerifyManager(HomekitAuthInfo authInfo, HomekitRegistry registry) {
this.authInfo = authInfo;
this.registry = registry;
}
public HttpResponse handle(HttpRequest rawRequest) throws Exception {
- PairVerificationRequest request = PairVerificationRequest.of(rawRequest.getBody());
- switch (request.getStage()) {
- case ONE:
- return stage1((Stage1Request) request);
+ PairVerifyRequest request = PairVerifyRequest.of(rawRequest.getBody());
+ switch (request.getState()) {
+ case 1:
+ return handleVerifyStartRequest((VerifyStartRequest) request);
- case TWO:
- return stage2((Stage2Request) request);
+ case 3:
+ return handleVerifyFinishRequest((VerifyFinishRequest) request);
default:
return new NotFoundResponse();
}
}
- private HttpResponse stage1(Stage1Request request) throws Exception {
+ private HttpResponse handleVerifyStartRequest(VerifyStartRequest request) throws Exception {
logger.trace("Starting pair verification for " + registry.getLabel());
clientPublicKey = request.getClientPublicKey();
publicKey = new byte[32];
@@ -96,7 +95,7 @@ private HttpResponse stage1(Stage1Request request) throws Exception {
return new PairingResponse(encoder.toByteArray());
}
- private HttpResponse stage2(Stage2Request request) throws Exception {
+ private HttpResponse handleVerifyFinishRequest(VerifyFinishRequest request) throws Exception {
ChachaDecoder chacha = new ChachaDecoder(hkdfKey, "PV-Msg03".getBytes(StandardCharsets.UTF_8));
byte[] plaintext = chacha.decodeCiphertext(request.getAuthTagData(), request.getMessageData());
@@ -110,7 +109,8 @@ private HttpResponse stage2(Stage2Request request) throws Exception {
authInfo.getUserPublicKey(
authInfo.getMac() + new String(clientUsername, StandardCharsets.UTF_8));
if (clientLtpk == null) {
- throw new Exception("Unknown user: " + new String(clientUsername, StandardCharsets.UTF_8));
+ logger.warn("Unknown user: {}", new String(clientUsername, StandardCharsets.UTF_8));
+ return new PairingResponse(4, ErrorCode.AUTHENTICATION);
}
Encoder encoder = TypeLengthValueUtils.getEncoder();
@@ -122,9 +122,8 @@ private HttpResponse stage2(Stage2Request request) throws Exception {
createKey("Control-Write-Encryption-Key"),
createKey("Control-Read-Encryption-Key"));
} else {
- encoder.add(MessageType.ERROR, (short) 4);
logger.warn("Invalid signature. Could not pair " + registry.getLabel());
- return new OkResponse(encoder.toByteArray());
+ return new PairingResponse(4, ErrorCode.AUTHENTICATION);
}
}
@@ -142,7 +141,7 @@ private byte[] createKey(String info) {
private static SecureRandom getSecureRandom() {
if (secureRandom == null) {
- synchronized (PairVerificationManager.class) {
+ synchronized (PairVerifyManager.class) {
if (secureRandom == null) {
secureRandom = new SecureRandom();
}
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationRequest.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyRequest.java
similarity index 61%
rename from src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationRequest.java
rename to src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyRequest.java
index 3bb6627d9..7cf31585b 100644
--- a/src/main/java/io/github/hapjava/server/impl/pairing/PairVerificationRequest.java
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairVerifyRequest.java
@@ -2,33 +2,32 @@
import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult;
-abstract class PairVerificationRequest {
+abstract class PairVerifyRequest {
- private static final short VALUE_STAGE_1 = 1;
- private static final short VALUE_STAGE_2 = 3;
-
- static PairVerificationRequest of(byte[] content) throws Exception {
+ static PairVerifyRequest of(byte[] content) throws Exception {
DecodeResult d = TypeLengthValueUtils.decode(content);
short stage = d.getByte(MessageType.STATE);
switch (stage) {
- case VALUE_STAGE_1:
- return new Stage1Request(d);
+ case 1:
+ return new VerifyStartRequest(d);
- case VALUE_STAGE_2:
- return new Stage2Request(d);
+ case 3:
+ return new VerifyFinishRequest(d);
default:
throw new Exception("Unknown pair process stage: " + stage);
}
}
- abstract Stage getStage();
+ // Raw integer.
+ // State of the pairing process. 1=M1, 2=M2, etc.
+ abstract int getState();
- static class Stage1Request extends PairVerificationRequest {
+ static class VerifyStartRequest extends PairVerifyRequest {
private final byte[] clientPublicKey;
- public Stage1Request(DecodeResult d) {
+ public VerifyStartRequest(DecodeResult d) {
clientPublicKey = d.getBytes(MessageType.PUBLIC_KEY);
}
@@ -37,17 +36,17 @@ public byte[] getClientPublicKey() {
}
@Override
- Stage getStage() {
- return Stage.ONE;
+ int getState() {
+ return 1;
}
}
- static class Stage2Request extends PairVerificationRequest {
+ static class VerifyFinishRequest extends PairVerifyRequest {
private final byte[] messageData;
private final byte[] authTagData;
- public Stage2Request(DecodeResult d) {
+ public VerifyFinishRequest(DecodeResult d) {
messageData = new byte[d.getLength(MessageType.ENCRYPTED_DATA) - 16];
authTagData = new byte[16];
d.getBytes(MessageType.ENCRYPTED_DATA, messageData, 0);
@@ -63,8 +62,8 @@ public byte[] getAuthTagData() {
}
@Override
- public Stage getStage() {
- return Stage.TWO;
+ public int getState() {
+ return 3;
}
}
}
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairingMethod.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairingMethod.java
new file mode 100644
index 000000000..07decf02d
--- /dev/null
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairingMethod.java
@@ -0,0 +1,24 @@
+package io.github.hapjava.server.impl.pairing;
+
+public enum PairingMethod {
+ PAIR_SETUP(0),
+ PAIR_SETUP_WITH_AUTH(1),
+ PAIR_VERIFY(2),
+ ADD_PAIRING(3),
+ REMOVE_PAIRING(4),
+ LIST_PAIRINGS(5);
+
+ private final byte value;
+
+ PairingMethod(byte value) {
+ this.value = value;
+ }
+
+ PairingMethod(int value) {
+ this.value = (byte) value;
+ }
+
+ public byte getValue() {
+ return value;
+ }
+}
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairingResponse.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairingResponse.java
index af678a86c..defc5798f 100644
--- a/src/main/java/io/github/hapjava/server/impl/pairing/PairingResponse.java
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairingResponse.java
@@ -21,6 +21,27 @@ public PairingResponse(byte[] body) {
super(body);
}
+ public PairingResponse(int state) {
+ this(encodeSuccess(state));
+ }
+
+ public PairingResponse(int state, ErrorCode errorCode) {
+ this(encodeError(state, errorCode));
+ }
+
+ private static byte[] encodeSuccess(int state) {
+ TypeLengthValueUtils.Encoder encoder = TypeLengthValueUtils.getEncoder();
+ encoder.add(MessageType.STATE, (byte) state);
+ return encoder.toByteArray();
+ }
+
+ private static byte[] encodeError(int state, ErrorCode errorCode) {
+ TypeLengthValueUtils.Encoder encoder = TypeLengthValueUtils.getEncoder();
+ encoder.add(MessageType.STATE, (byte) state);
+ encoder.add(MessageType.ERROR, (byte) errorCode.getKey());
+ return encoder.toByteArray();
+ }
+
@Override
public Map getHeaders() {
return headers;
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java
deleted file mode 100644
index 3d52fe907..000000000
--- a/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package io.github.hapjava.server.impl.pairing;
-
-import io.github.hapjava.server.HomekitAuthInfo;
-import io.github.hapjava.server.impl.http.HttpRequest;
-import io.github.hapjava.server.impl.http.HttpResponse;
-import io.github.hapjava.server.impl.jmdns.JmdnsHomekitAdvertiser;
-import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-
-public class PairingUpdateController {
-
- private final HomekitAuthInfo authInfo;
- private final JmdnsHomekitAdvertiser advertiser;
-
- public PairingUpdateController(HomekitAuthInfo authInfo, JmdnsHomekitAdvertiser advertiser) {
- this.authInfo = authInfo;
- this.advertiser = advertiser;
- }
-
- public HttpResponse handle(HttpRequest request) throws IOException {
- DecodeResult d = TypeLengthValueUtils.decode(request.getBody());
-
- int method = d.getByte(MessageType.METHOD);
- if (method == 3) { // Add pairing
- byte[] username = d.getBytes(MessageType.USERNAME);
- byte[] ltpk = d.getBytes(MessageType.PUBLIC_KEY);
- authInfo.createUser(authInfo.getMac() + new String(username, StandardCharsets.UTF_8), ltpk);
- } else if (method == 4) { // Remove pairing
- byte[] username = d.getBytes(MessageType.USERNAME);
- authInfo.removeUser(authInfo.getMac() + new String(username, StandardCharsets.UTF_8));
- if (!authInfo.hasUser()) {
- advertiser.setDiscoverable(true);
- }
- } else {
- throw new RuntimeException("Unrecognized method: " + method);
- }
- return new PairingResponse(new byte[] {0x06, 0x01, 0x02});
- }
-}
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/PairingsManager.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairingsManager.java
new file mode 100644
index 000000000..11eb4a709
--- /dev/null
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairingsManager.java
@@ -0,0 +1,77 @@
+package io.github.hapjava.server.impl.pairing;
+
+import io.github.hapjava.server.HomekitAuthInfo;
+import io.github.hapjava.server.impl.http.HttpRequest;
+import io.github.hapjava.server.impl.http.HttpResponse;
+import io.github.hapjava.server.impl.jmdns.JmdnsHomekitAdvertiser;
+import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Iterator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PairingsManager {
+
+ private final HomekitAuthInfo authInfo;
+ private final JmdnsHomekitAdvertiser advertiser;
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PairingsManager.class);
+
+ public PairingsManager(HomekitAuthInfo authInfo, JmdnsHomekitAdvertiser advertiser) {
+ this.authInfo = authInfo;
+ this.advertiser = advertiser;
+ }
+
+ public HttpResponse handle(HttpRequest request) throws IOException {
+ DecodeResult d = TypeLengthValueUtils.decode(request.getBody());
+
+ int method = d.getByte(MessageType.METHOD);
+
+ if (method == PairingMethod.ADD_PAIRING.getValue()) {
+ byte[] username = d.getBytes(MessageType.USERNAME);
+ byte[] ltpk = d.getBytes(MessageType.PUBLIC_KEY);
+ byte permissions = d.getByte(MessageType.PERMISSIONS);
+ authInfo.createUser(
+ authInfo.getMac() + new String(username, StandardCharsets.UTF_8), ltpk, permissions == 1);
+ } else if (method == PairingMethod.REMOVE_PAIRING.getValue()) {
+ byte[] username = d.getBytes(MessageType.USERNAME);
+ authInfo.removeUser(authInfo.getMac() + new String(username, StandardCharsets.UTF_8));
+ if (!authInfo.hasUser()) {
+ advertiser.setDiscoverable(true);
+ }
+ } else if (method == PairingMethod.LIST_PAIRINGS.getValue()) {
+ TypeLengthValueUtils.Encoder e = TypeLengthValueUtils.getEncoder();
+
+ Collection usernames = authInfo.listUsers();
+ boolean first = true;
+ Iterator iterator = usernames.iterator();
+ String mac = authInfo.getMac();
+
+ while (iterator.hasNext()) {
+ String username = iterator.next();
+ if (first) {
+ e.add(MessageType.STATE, (byte) 2);
+ first = false;
+ } else {
+ e.add(MessageType.SEPARATOR);
+ }
+ byte[] publicKey = authInfo.getUserPublicKey(username);
+ boolean isAdmin = authInfo.userIsAdmin(username);
+ if (username.startsWith(mac)) {
+ username = username.substring(mac.length());
+ }
+ e.add(MessageType.USERNAME, username);
+ e.add(MessageType.PUBLIC_KEY, publicKey);
+ e.add(MessageType.PERMISSIONS, (short) (isAdmin ? 1 : 0));
+ }
+
+ return new PairingResponse(e.toByteArray());
+ } else {
+ throw new RuntimeException("Unrecognized method: " + method);
+ }
+
+ return new PairingResponse(2);
+ }
+}
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/SrpHandler.java b/src/main/java/io/github/hapjava/server/impl/pairing/SrpHandler.java
index e02ccdb90..5c1c23353 100644
--- a/src/main/java/io/github/hapjava/server/impl/pairing/SrpHandler.java
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/SrpHandler.java
@@ -3,7 +3,7 @@
import com.nimbusds.srp6.*;
import io.github.hapjava.server.impl.http.HttpResponse;
import io.github.hapjava.server.impl.pairing.HomekitSRP6ServerSession.State;
-import io.github.hapjava.server.impl.pairing.PairSetupRequest.Stage2Request;
+import io.github.hapjava.server.impl.pairing.PairSetupRequest.SRPVerifyRequest;
import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.Encoder;
import io.github.hapjava.server.impl.responses.ConflictResponse;
import io.github.hapjava.server.impl.responses.NotFoundResponse;
@@ -39,19 +39,19 @@ public SrpHandler(String pin, BigInteger salt) {
}
public HttpResponse handle(PairSetupRequest request) throws Exception {
- switch (request.getStage()) {
- case ONE:
- return step1();
+ switch (request.getState()) {
+ case 1:
+ return handleSrpStartRequest();
- case TWO:
- return step2((Stage2Request) request);
+ case 3:
+ return handleSrpVerifyRequest((SRPVerifyRequest) request);
default:
return new NotFoundResponse();
}
}
- private HttpResponse step1() throws Exception {
+ private HttpResponse handleSrpStartRequest() throws Exception {
if (session.getState() != State.INIT) {
logger.warn("Session is not in state INIT when receiving step1");
return new ConflictResponse();
@@ -68,7 +68,7 @@ private HttpResponse step1() throws Exception {
return new PairingResponse(encoder.toByteArray());
}
- private HttpResponse step2(Stage2Request request) throws Exception {
+ private HttpResponse handleSrpVerifyRequest(SRPVerifyRequest request) throws Exception {
if (session.getState() != State.STEP_1) {
logger.warn("Session is not in state Stage 1 when receiving step2");
return new ConflictResponse();
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/Stage.java b/src/main/java/io/github/hapjava/server/impl/pairing/Stage.java
deleted file mode 100644
index 19bac9a14..000000000
--- a/src/main/java/io/github/hapjava/server/impl/pairing/Stage.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package io.github.hapjava.server.impl.pairing;
-
-public enum Stage {
- ONE,
- TWO,
- THREE
-}
diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java b/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java
index 396829d34..cbd76f118 100644
--- a/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java
+++ b/src/main/java/io/github/hapjava/server/impl/pairing/TypeLengthValueUtils.java
@@ -5,6 +5,8 @@
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@@ -37,6 +39,11 @@ private Encoder() {
baos = new ByteArrayOutputStream();
}
+ public void add(MessageType type) {
+ baos.write(type.getKey());
+ baos.write(0);
+ }
+
public void add(MessageType type, BigInteger i) throws IOException {
add(type, ByteUtils.toByteArray(i));
}
@@ -58,6 +65,10 @@ public void add(MessageType type, byte[] bytes) throws IOException {
}
}
+ public void add(MessageType type, String string) throws IOException {
+ add(type, string.getBytes(StandardCharsets.UTF_8));
+ }
+
public byte[] toByteArray() {
return baos.toByteArray();
}
@@ -68,10 +79,23 @@ public static final class DecodeResult {
private DecodeResult() {}
+ public String toString() {
+ return result.toString();
+ }
+
+ public boolean hasMessage(MessageType type) {
+ return result.containsKey(type.getKey());
+ }
+
public byte getByte(MessageType type) {
return result.get(type.getKey())[0];
}
+ public int getInt(MessageType type) {
+ ByteBuffer wrapped = ByteBuffer.wrap(result.get(type.getKey()));
+ return wrapped.getInt();
+ }
+
public BigInteger getBigInt(MessageType type) {
return new BigInteger(1, result.get(type.getKey()));
}