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())); }