Skip to content

Commit 5d57643

Browse files
authored
Merge pull request #175 from ccutrer/list-pairings
improve pairing reliability with iOS 16.2
2 parents 036e05a + d1d6fb0 commit 5d57643

19 files changed

+359
-150
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# HAP-Java 2.0.5
2+
* Implement List-Pairings method. Compatibility with new Home infrastructure from iOS 16.2?
3+
14
# HAP-Java 2.0.3
25
* Avoid unnecessary forced disconnects. Library users should be updating the configuration index anyway.
36

src/main/java/io/github/hapjava/server/HomekitAuthInfo.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import io.github.hapjava.server.impl.HomekitServer;
44
import io.github.hapjava.server.impl.crypto.HAPSetupCodeUtils;
55
import java.math.BigInteger;
6+
import java.util.Collection;
7+
import java.util.List;
68

79
/**
810
* Authentication info that must be provided when constructing a new {@link HomekitServer}. You will
@@ -65,8 +67,23 @@ default String getSetupId() {
6567
* @param username the iOS device's username. The value will not be meaningful to anything but
6668
* iOS.
6769
* @param publicKey the iOS device's public key.
70+
* @param isAdmin if the user is an admin, authorized to and/remove other users
6871
*/
69-
void createUser(String username, byte[] publicKey);
72+
default void createUser(String username, byte[] publicKey, boolean isAdmin) {
73+
createUser(username, publicKey);
74+
}
75+
76+
/**
77+
* Deprecated method to add a user, assuming all users are admins.
78+
*
79+
* <p>At least one of the createUser methods must be implemented.
80+
*
81+
* @param username the iOS device's username.
82+
* @param publicKey the iOS device's public key.
83+
*/
84+
default void createUser(String username, byte[] publicKey) {
85+
createUser(username, publicKey, true);
86+
}
7087

7188
/**
7289
* Called when an iOS device needs to remove an existing pairing. Subsequent calls to {@link
@@ -76,6 +93,15 @@ default String getSetupId() {
7693
*/
7794
void removeUser(String username);
7895

96+
/**
97+
* List all users which have been authenticated.
98+
*
99+
* @return the previously stored list of users.
100+
*/
101+
default Collection<String> listUsers() {
102+
return List.of();
103+
}
104+
79105
/**
80106
* Called when an already paired iOS device is re-connecting. The public key returned by this
81107
* method will be compared with the signature of the pair verification request to validate the
@@ -86,6 +112,16 @@ default String getSetupId() {
86112
*/
87113
byte[] getUserPublicKey(String username);
88114

115+
/**
116+
* Determine if the specified user is an admin.
117+
*
118+
* @param username the username of the iOS device to retrieve permissions for.
119+
* @return the previously stored permissions.
120+
*/
121+
default boolean userIsAdmin(String username) {
122+
return true;
123+
}
124+
89125
/**
90126
* Called to check if a user has been created. The homekit accessory advertises whether the
91127
* accessory has already been paired. At this time, it's unclear whether multiple users can be

src/main/java/io/github/hapjava/server/impl/HomekitRoot.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ public void stop() {
197197
* @throws IOException if there is an error in the underlying protocol, such as a TCP error
198198
*/
199199
public void refreshAuthInfo() throws IOException {
200+
advertiser.setMac(authInfo.getMac());
200201
advertiser.setDiscoverable(!authInfo.hasUser());
201202
}
202203

src/main/java/io/github/hapjava/server/impl/connections/HttpSession.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
import io.github.hapjava.server.impl.jmdns.JmdnsHomekitAdvertiser;
1010
import io.github.hapjava.server.impl.json.AccessoryController;
1111
import io.github.hapjava.server.impl.json.CharacteristicsController;
12-
import io.github.hapjava.server.impl.pairing.PairVerificationManager;
13-
import io.github.hapjava.server.impl.pairing.PairingManager;
14-
import io.github.hapjava.server.impl.pairing.PairingUpdateController;
12+
import io.github.hapjava.server.impl.pairing.PairSetupManager;
13+
import io.github.hapjava.server.impl.pairing.PairVerifyManager;
14+
import io.github.hapjava.server.impl.pairing.PairingsManager;
1515
import io.github.hapjava.server.impl.responses.InternalServerErrorResponse;
1616
import io.github.hapjava.server.impl.responses.NotFoundResponse;
1717
import java.io.IOException;
@@ -21,8 +21,8 @@
2121

2222
class HttpSession {
2323

24-
private volatile PairingManager pairingManager;
25-
private volatile PairVerificationManager pairVerificationManager;
24+
private volatile PairSetupManager pairSetupManager;
25+
private volatile PairVerifyManager pairVerifyManager;
2626
private volatile AccessoryController accessoryController;
2727
private volatile CharacteristicsController characteristicsController;
2828

@@ -67,7 +67,7 @@ public HttpResponse handleRequest(HttpRequest request) throws IOException {
6767

6868
public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOException {
6969
advertiser.setDiscoverable(
70-
false); // brigde is already bound and should not be discoverable anymore
70+
false); // bridge is already bound and should not be discoverable anymore
7171
try {
7272
switch (request.getUri()) {
7373
case "/accessories":
@@ -84,7 +84,7 @@ public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOExc
8484
}
8585

8686
case "/pairings":
87-
return new PairingUpdateController(authInfo, advertiser).handle(request);
87+
return new PairingsManager(authInfo, advertiser).handle(request);
8888

8989
default:
9090
if (request.getUri().startsWith("/characteristics?")) {
@@ -100,31 +100,31 @@ public HttpResponse handleAuthenticatedRequest(HttpRequest request) throws IOExc
100100
}
101101

102102
private HttpResponse handlePairSetup(HttpRequest request) {
103-
if (pairingManager == null) {
103+
if (pairSetupManager == null) {
104104
synchronized (HttpSession.class) {
105-
if (pairingManager == null) {
106-
pairingManager = new PairingManager(authInfo, registry);
105+
if (pairSetupManager == null) {
106+
pairSetupManager = new PairSetupManager(authInfo, registry);
107107
}
108108
}
109109
}
110110
try {
111-
return pairingManager.handle(request);
111+
return pairSetupManager.handle(request);
112112
} catch (Exception e) {
113113
logger.warn("Exception encountered during pairing", e);
114114
return new InternalServerErrorResponse(e);
115115
}
116116
}
117117

118118
private HttpResponse handlePairVerify(HttpRequest request) {
119-
if (pairVerificationManager == null) {
119+
if (pairVerifyManager == null) {
120120
synchronized (HttpSession.class) {
121-
if (pairVerificationManager == null) {
122-
pairVerificationManager = new PairVerificationManager(authInfo, registry);
121+
if (pairVerifyManager == null) {
122+
pairVerifyManager = new PairVerifyManager(authInfo, registry);
123123
}
124124
}
125125
}
126126
try {
127-
return pairVerificationManager.handle(request);
127+
return pairVerifyManager.handle(request);
128128
} catch (Exception e) {
129129
logger.warn("Exception encountered while verifying pairing", e);
130130
return new InternalServerErrorResponse(e);

src/main/java/io/github/hapjava/server/impl/jmdns/JmdnsHomekitAdvertiser.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ public synchronized void setDiscoverable(boolean discoverable) throws IOExceptio
7979
}
8080
}
8181

82+
public synchronized void setMac(String mac) throws IOException {
83+
if (this.mac != mac) {
84+
this.mac = mac;
85+
if (isAdvertising) {
86+
logger.trace("Re-creating service due to change in mac to " + mac);
87+
unregisterService();
88+
registerService();
89+
}
90+
}
91+
}
92+
8293
public synchronized void setConfigurationIndex(int revision) throws IOException {
8394
if (this.configurationIndex != revision) {
8495
this.configurationIndex = revision;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.github.hapjava.server.impl.pairing;
2+
3+
public enum ErrorCode {
4+
OK(0),
5+
UNKNOWN(1),
6+
AUTHENTICATION(2),
7+
BACKOFF(3),
8+
MAX_PEERS(4),
9+
MAX_TRIES(5),
10+
UNAVAILABLE(6),
11+
BUSY(7);
12+
13+
private final short key;
14+
15+
ErrorCode(short key) {
16+
this.key = key;
17+
}
18+
19+
ErrorCode(int key) {
20+
this.key = (short) key;
21+
}
22+
23+
public short getKey() {
24+
return key;
25+
}
26+
}

src/main/java/io/github/hapjava/server/impl/pairing/FinalPairHandler.java renamed to src/main/java/io/github/hapjava/server/impl/pairing/ExchangeHandler.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,26 @@
66
import io.github.hapjava.server.impl.crypto.EdsaSigner;
77
import io.github.hapjava.server.impl.crypto.EdsaVerifier;
88
import io.github.hapjava.server.impl.http.HttpResponse;
9-
import io.github.hapjava.server.impl.pairing.PairSetupRequest.Stage3Request;
9+
import io.github.hapjava.server.impl.pairing.PairSetupRequest.ExchangeRequest;
1010
import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult;
1111
import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.Encoder;
1212
import java.nio.charset.StandardCharsets;
1313
import org.bouncycastle.crypto.digests.SHA512Digest;
1414
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
1515
import org.bouncycastle.crypto.params.HKDFParameters;
16+
import org.slf4j.Logger;
17+
import org.slf4j.LoggerFactory;
1618

17-
class FinalPairHandler {
19+
class ExchangeHandler {
1820

1921
private final byte[] k;
2022
private final HomekitAuthInfo authInfo;
2123

2224
private byte[] hkdf_enc_key;
2325

24-
public FinalPairHandler(byte[] k, HomekitAuthInfo authInfo) {
26+
private static final Logger LOGGER = LoggerFactory.getLogger(ExchangeHandler.class);
27+
28+
public ExchangeHandler(byte[] k, HomekitAuthInfo authInfo) {
2529
this.k = k;
2630
this.authInfo = authInfo;
2731
}
@@ -36,10 +40,10 @@ public HttpResponse handle(PairSetupRequest req) throws Exception {
3640
byte[] okm = hkdf_enc_key = new byte[32];
3741
hkdf.generateBytes(okm, 0, 32);
3842

39-
return decrypt((Stage3Request) req, okm);
43+
return decrypt((ExchangeRequest) req, okm);
4044
}
4145

42-
private HttpResponse decrypt(Stage3Request req, byte[] key) throws Exception {
46+
private HttpResponse decrypt(ExchangeRequest req, byte[] key) throws Exception {
4347
ChachaDecoder chacha = new ChachaDecoder(key, "PS-Msg05".getBytes(StandardCharsets.UTF_8));
4448
byte[] plaintext = chacha.decodeCiphertext(req.getAuthTagData(), req.getMessageData());
4549

@@ -63,9 +67,11 @@ private HttpResponse createUser(byte[] username, byte[] ltpk, byte[] proof) thro
6367
byte[] completeData = ByteUtils.joinBytes(okm, username, ltpk);
6468

6569
if (!new EdsaVerifier(ltpk).verify(completeData, proof)) {
66-
throw new Exception("Invalid signature");
70+
return new PairingResponse(6, ErrorCode.AUTHENTICATION);
6771
}
68-
authInfo.createUser(authInfo.getMac() + new String(username, StandardCharsets.UTF_8), ltpk);
72+
String stringUsername = new String(username, StandardCharsets.UTF_8);
73+
LOGGER.trace("Creating initial user {}", stringUsername);
74+
authInfo.createUser(authInfo.getMac() + stringUsername, ltpk, true);
6975
return createResponse();
7076
}
7177

src/main/java/io/github/hapjava/server/impl/pairing/MessageType.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ public enum MessageType {
99
ENCRYPTED_DATA(5),
1010
STATE(6),
1111
ERROR(7),
12-
SIGNATURE(10);
12+
SIGNATURE(0x0a),
13+
PERMISSIONS(0x0b),
14+
FRAGMENT_DATA(0x0c),
15+
FRAGMENT_LAST(0x0d),
16+
FLAGS(0x13),
17+
SEPARATOR(0xff);
1318

1419
private final short key;
1520

src/main/java/io/github/hapjava/server/impl/pairing/PairingManager.java renamed to src/main/java/io/github/hapjava/server/impl/pairing/PairSetupManager.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,48 +9,49 @@
99
import org.slf4j.Logger;
1010
import org.slf4j.LoggerFactory;
1111

12-
public class PairingManager {
12+
public class PairSetupManager {
1313

14-
private static final Logger logger = LoggerFactory.getLogger(PairingManager.class);
14+
private static final Logger logger = LoggerFactory.getLogger(PairSetupManager.class);
1515

1616
private final HomekitAuthInfo authInfo;
1717
private final HomekitRegistry registry;
1818

1919
private SrpHandler srpHandler;
2020

21-
public PairingManager(HomekitAuthInfo authInfo, HomekitRegistry registry) {
21+
public PairSetupManager(HomekitAuthInfo authInfo, HomekitRegistry registry) {
2222
this.authInfo = authInfo;
2323
this.registry = registry;
2424
}
2525

2626
public HttpResponse handle(HttpRequest httpRequest) throws Exception {
2727
PairSetupRequest req = PairSetupRequest.of(httpRequest.getBody());
28+
logger.trace("Handling pair-setup request {}", req);
2829

29-
if (req.getStage() == Stage.ONE) {
30-
logger.trace("Starting pair for " + registry.getLabel());
30+
if (req.getState() == 1) {
31+
logger.trace("Received SRP Start Request " + registry.getLabel());
3132
srpHandler = new SrpHandler(authInfo.getPin(), authInfo.getSalt());
3233
return srpHandler.handle(req);
33-
} else if (req.getStage() == Stage.TWO) {
34-
logger.trace("Entering second stage of pair for " + registry.getLabel());
34+
} else if (req.getState() == 3) {
35+
logger.trace("Receive SRP Verify Request for " + registry.getLabel());
3536
if (srpHandler == null) {
36-
logger.warn("Received unexpected stage 2 request for " + registry.getLabel());
37+
logger.warn("Received unexpected SRP Verify Request for " + registry.getLabel());
3738
return new UnauthorizedResponse();
3839
} else {
3940
try {
4041
return srpHandler.handle(req);
4142
} catch (Exception e) {
4243
srpHandler = null; // You don't get to try again - need a new key
43-
logger.warn("Exception encountered while processing pairing request", e);
44+
logger.warn("Exception encountered while processing SRP Verify Request", e);
4445
return new UnauthorizedResponse();
4546
}
4647
}
47-
} else if (req.getStage() == Stage.THREE) {
48-
logger.trace("Entering third stage of pair for " + registry.getLabel());
48+
} else if (req.getState() == 5) {
49+
logger.trace("Received Exchange Request for " + registry.getLabel());
4950
if (srpHandler == null) {
50-
logger.warn("Received unexpected stage 3 request for " + registry.getLabel());
51+
logger.warn("Received unexpected Exchanged Request for " + registry.getLabel());
5152
return new UnauthorizedResponse();
5253
} else {
53-
FinalPairHandler handler = new FinalPairHandler(srpHandler.getK(), authInfo);
54+
ExchangeHandler handler = new ExchangeHandler(srpHandler.getK(), authInfo);
5455
try {
5556
return handler.handle(req);
5657
} catch (Exception e) {

0 commit comments

Comments
 (0)