Skip to content

Commit d1d6fb0

Browse files
committed
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
1 parent 9c63588 commit d1d6fb0

File tree

14 files changed

+209
-114
lines changed

14 files changed

+209
-114
lines changed

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: 14 additions & 14 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

@@ -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 & 8 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,10 +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(
69-
authInfo.getMac() + new String(username, StandardCharsets.UTF_8), ltpk, true);
72+
String stringUsername = new String(username, StandardCharsets.UTF_8);
73+
LOGGER.trace("Creating initial user {}", stringUsername);
74+
authInfo.createUser(authInfo.getMac() + stringUsername, ltpk, true);
7075
return createResponse();
7176
}
7277

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) {

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

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,61 @@
22

33
import io.github.hapjava.server.impl.pairing.TypeLengthValueUtils.DecodeResult;
44
import java.math.BigInteger;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
57

68
abstract class PairSetupRequest {
7-
8-
private static final short VALUE_STAGE_1 = 1;
9-
private static final short VALUE_STAGE_2 = 3;
10-
private static final short VALUE_STAGE_3 = 5;
9+
private static final Logger logger = LoggerFactory.getLogger(PairSetupRequest.class);
1110

1211
public static PairSetupRequest of(byte[] content) throws Exception {
1312
DecodeResult d = TypeLengthValueUtils.decode(content);
13+
logger.trace("Decoded pair setup request: {}", d);
1414
short stage = d.getByte(MessageType.STATE);
1515
switch (stage) {
16-
case VALUE_STAGE_1:
17-
return new Stage1Request();
16+
case 1:
17+
return new SRPStartRequest(d);
1818

19-
case VALUE_STAGE_2:
20-
return new Stage2Request(d);
19+
case 3:
20+
return new SRPVerifyRequest(d);
2121

22-
case VALUE_STAGE_3:
23-
return new Stage3Request(d);
22+
case 5:
23+
return new ExchangeRequest(d);
2424

2525
default:
2626
throw new Exception("Unknown pair process stage: " + stage);
2727
}
2828
}
2929

30-
public abstract Stage getStage();
30+
// Raw integer.
31+
// State of the pairing process. 1=M1, 2=M2, etc.
32+
public abstract int getState();
33+
34+
public static class SRPStartRequest extends PairSetupRequest {
35+
int flags;
36+
37+
public SRPStartRequest(DecodeResult d) {
38+
flags = 0;
39+
if (d.hasMessage(MessageType.FLAGS)) {
40+
flags = d.getInt(MessageType.FLAGS);
41+
}
42+
}
3143

32-
public static class Stage1Request extends PairSetupRequest {
3344
@Override
34-
public Stage getStage() {
35-
return Stage.ONE;
45+
public int getState() {
46+
return 1;
47+
}
48+
49+
public String toString() {
50+
return "<M1 flags=" + Integer.toString(flags) + ">";
3651
}
3752
}
3853

39-
public static class Stage2Request extends PairSetupRequest {
54+
public static class SRPVerifyRequest extends PairSetupRequest {
4055

4156
private final BigInteger a;
4257
private final BigInteger m1;
4358

44-
public Stage2Request(DecodeResult d) {
59+
public SRPVerifyRequest(DecodeResult d) {
4560
a = d.getBigInt(MessageType.PUBLIC_KEY);
4661
m1 = d.getBigInt(MessageType.PROOF);
4762
}
@@ -55,17 +70,21 @@ public BigInteger getM1() {
5570
}
5671

5772
@Override
58-
public Stage getStage() {
59-
return Stage.TWO;
73+
public int getState() {
74+
return 3;
75+
}
76+
77+
public String toString() {
78+
return "<M3 public key=" + a.toString() + ", proof=" + m1.toString() + ">";
6079
}
6180
}
6281

63-
static class Stage3Request extends PairSetupRequest {
82+
static class ExchangeRequest extends PairSetupRequest {
6483

6584
private final byte[] messageData;
6685
private final byte[] authTagData;
6786

68-
public Stage3Request(DecodeResult d) {
87+
public ExchangeRequest(DecodeResult d) {
6988
messageData = new byte[d.getLength(MessageType.ENCRYPTED_DATA) - 16];
7089
authTagData = new byte[16];
7190
d.getBytes(MessageType.ENCRYPTED_DATA, messageData, 0);
@@ -81,8 +100,12 @@ public byte[] getAuthTagData() {
81100
}
82101

83102
@Override
84-
public Stage getStage() {
85-
return Stage.THREE;
103+
public int getState() {
104+
return 5;
105+
}
106+
107+
public String toString() {
108+
return "<M5>";
86109
}
87110
}
88111
}

0 commit comments

Comments
 (0)