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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
38 changes: 37 additions & 1 deletion src/main/java/io/github/hapjava/server/HomekitAuthInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*
* <p>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
Expand All @@ -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<String> 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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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":
Expand All @@ -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?")) {
Expand All @@ -100,31 +100,31 @@ 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);
}
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/io/github/hapjava/server/impl/pairing/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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());

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading