From f04a4d6345b6dd94f7ed19874f98b539742c9126 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 16 Dec 2022 14:22:29 -0700 Subject: [PATCH 1/3] implement List Pairings request also keep track of admin permissions for users note that those permissions aren't actually enforced yet --- CHANGES.md | 3 ++ .../hapjava/server/HomekitAuthInfo.java | 38 ++++++++++++++++++- .../server/impl/connections/HttpSession.java | 2 +- .../server/impl/pairing/FinalPairHandler.java | 3 +- .../server/impl/pairing/MessageType.java | 7 +++- .../server/impl/pairing/PairingMethod.java | 24 ++++++++++++ .../impl/pairing/PairingUpdateController.java | 36 ++++++++++++++++-- .../impl/pairing/TypeLengthValueUtils.java | 10 +++++ 8 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 src/main/java/io/github/hapjava/server/impl/pairing/PairingMethod.java 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/connections/HttpSession.java b/src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java index 36ac04085..08a49f64a 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 @@ -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": diff --git a/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java b/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java index e5d9e371c..9d464147a 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java @@ -65,7 +65,8 @@ private HttpResponse createUser(byte[] username, byte[] ltpk, byte[] proof) thro if (!new EdsaVerifier(ltpk).verify(completeData, proof)) { throw new Exception("Invalid signature"); } - authInfo.createUser(authInfo.getMac() + new String(username, StandardCharsets.UTF_8), ltpk); + authInfo.createUser( + authInfo.getMac() + new String(username, StandardCharsets.UTF_8), 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/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/PairingUpdateController.java b/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java index 3d52fe907..abc73dfc1 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java @@ -7,6 +7,8 @@ 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; public class PairingUpdateController { @@ -22,19 +24,45 @@ public HttpResponse handle(HttpRequest request) throws IOException { DecodeResult d = TypeLengthValueUtils.decode(request.getBody()); int method = d.getByte(MessageType.METHOD); - if (method == 3) { // Add pairing + if (method == PairingMethod.ADD_PAIRING.getValue()) { 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 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(); + while (iterator.hasNext()) { + String username = iterator.next(); + if (first) { + e.add(MessageType.STATE, (byte) 2); + first = false; + } else { + e.add(MessageType.SEPARATOR); + } + e.add(MessageType.USERNAME, username); + e.add(MessageType.PUBLIC_KEY, authInfo.getUserPublicKey(username)); + e.add(MessageType.PERMISSIONS, (short) (authInfo.userIsAdmin(username) ? 1 : 0)); + } + ; + + return new PairingResponse(e.toByteArray()); } else { throw new RuntimeException("Unrecognized method: " + method); } - return new PairingResponse(new byte[] {0x06, 0x01, 0x02}); + + TypeLengthValueUtils.Encoder e = TypeLengthValueUtils.getEncoder(); + e.add(MessageType.STATE, (byte) 2); + return new PairingResponse(e.toByteArray()); } } 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..e36f81af8 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,7 @@ import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -37,6 +38,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 +64,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(); } From 9c6358873e30fff25296d728cf242a842089c5c1 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Sat, 17 Dec 2022 20:12:32 -0700 Subject: [PATCH 2/3] be sure to remove MAC from username before returning to iOS --- .../server/impl/pairing/PairingUpdateController.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 index abc73dfc1..d9cdf2f05 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java @@ -42,6 +42,8 @@ public HttpResponse handle(HttpRequest request) throws IOException { Collection usernames = authInfo.listUsers(); boolean first = true; Iterator iterator = usernames.iterator(); + String mac = authInfo.getMac(); + while (iterator.hasNext()) { String username = iterator.next(); if (first) { @@ -50,9 +52,14 @@ public HttpResponse handle(HttpRequest request) throws IOException { } 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, authInfo.getUserPublicKey(username)); - e.add(MessageType.PERMISSIONS, (short) (authInfo.userIsAdmin(username) ? 1 : 0)); + e.add(MessageType.PUBLIC_KEY, publicKey); + e.add(MessageType.PERMISSIONS, (short) (isAdmin ? 1 : 0)); } ; From d1d6fb09b4175cbabad181d2ee1f762b4111aaa9 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 19 Dec 2022 13:35:40 -0700 Subject: [PATCH 3/3] refactor pairing to match HAP spec more closely mostly renames, but significant change is that when we don't recognize a user, return an error code instead of throwing an exception and returning a 500 with no content. several responses are now generated with PairingResponse, rather than as a raw byte array, so that the reader can more easily follow what the response is. several more trace loggings showing the details of the pairing process have been added, for debug use also ensure that we check if the MAC has changed when refreshing auth info --- .../hapjava/server/impl/HomekitRoot.java | 1 + .../server/impl/connections/HttpSession.java | 28 ++++---- .../impl/jmdns/JmdnsHomekitAdvertiser.java | 11 +++ .../server/impl/pairing/ErrorCode.java | 26 +++++++ ...lPairHandler.java => ExchangeHandler.java} | 21 +++--- ...ringManager.java => PairSetupManager.java} | 27 ++++---- .../server/impl/pairing/PairSetupRequest.java | 67 +++++++++++++------ ...ionManager.java => PairVerifyManager.java} | 35 +++++----- ...ionRequest.java => PairVerifyRequest.java} | 35 +++++----- .../server/impl/pairing/PairingResponse.java | 21 ++++++ ...teController.java => PairingsManager.java} | 14 ++-- .../server/impl/pairing/SrpHandler.java | 16 ++--- .../hapjava/server/impl/pairing/Stage.java | 7 -- .../impl/pairing/TypeLengthValueUtils.java | 14 ++++ 14 files changed, 209 insertions(+), 114 deletions(-) create mode 100644 src/main/java/io/github/hapjava/server/impl/pairing/ErrorCode.java rename src/main/java/io/github/hapjava/server/impl/pairing/{FinalPairHandler.java => ExchangeHandler.java} (84%) rename src/main/java/io/github/hapjava/server/impl/pairing/{PairingManager.java => PairSetupManager.java} (64%) rename src/main/java/io/github/hapjava/server/impl/pairing/{PairVerificationManager.java => PairVerifyManager.java} (82%) rename src/main/java/io/github/hapjava/server/impl/pairing/{PairVerificationRequest.java => PairVerifyRequest.java} (61%) rename src/main/java/io/github/hapjava/server/impl/pairing/{PairingUpdateController.java => PairingsManager.java} (89%) delete mode 100644 src/main/java/io/github/hapjava/server/impl/pairing/Stage.java 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 08a49f64a..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; @@ -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 9d464147a..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,10 +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, true); + 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/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/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/PairingsManager.java similarity index 89% rename from src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java rename to src/main/java/io/github/hapjava/server/impl/pairing/PairingsManager.java index d9cdf2f05..11eb4a709 100644 --- a/src/main/java/io/github/hapjava/server/impl/pairing/PairingUpdateController.java +++ b/src/main/java/io/github/hapjava/server/impl/pairing/PairingsManager.java @@ -9,13 +9,17 @@ import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Iterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -public class PairingUpdateController { +public class PairingsManager { private final HomekitAuthInfo authInfo; private final JmdnsHomekitAdvertiser advertiser; - public PairingUpdateController(HomekitAuthInfo authInfo, JmdnsHomekitAdvertiser advertiser) { + private static final Logger LOGGER = LoggerFactory.getLogger(PairingsManager.class); + + public PairingsManager(HomekitAuthInfo authInfo, JmdnsHomekitAdvertiser advertiser) { this.authInfo = authInfo; this.advertiser = advertiser; } @@ -24,6 +28,7 @@ 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); @@ -61,15 +66,12 @@ public HttpResponse handle(HttpRequest request) throws IOException { 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); } - TypeLengthValueUtils.Encoder e = TypeLengthValueUtils.getEncoder(); - e.add(MessageType.STATE, (byte) 2); - return new PairingResponse(e.toByteArray()); + 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 e36f81af8..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,7 @@ 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; @@ -78,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())); }