package com.beowulfe.hap.impl.connections;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Collection;
import java.util.function.Consumer;
import org.bouncycastle.util.Pack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.beowulfe.hap.HomekitAuthInfo;
import com.beowulfe.hap.impl.HomekitRegistry;
import com.beowulfe.hap.impl.crypto.ChachaDecoder;
import com.beowulfe.hap.impl.crypto.ChachaEncoder;
import com.beowulfe.hap.impl.http.*;
import com.beowulfe.hap.impl.jmdns.JmdnsHomekitAdvertiser;
import com.beowulfe.hap.impl.pairing.UpgradeResponse;
class ConnectionImpl implements HomekitClientConnection {
private final HttpSession httpSession;
private LengthPrefixedByteArrayProcessor binaryProcessor;
private int inboundBinaryMessageCount = 0;
private int outboundBinaryMessageCount = 0;
private byte[] readKey;
private byte[] writeKey;
private boolean isUpgraded = false;
private final Consumer<HttpResponse> outOfBandMessageCallback;
private final SubscriptionManager subscriptions;
private final static Logger LOGGER = LoggerFactory.getLogger(HomekitClientConnection.class);
public ConnectionImpl(HomekitAuthInfo authInfo, HomekitRegistry registry,
Consumer<HttpResponse> outOfBandMessageCallback, SubscriptionManager subscriptions,
JmdnsHomekitAdvertiser advertiser) {
httpSession = new HttpSession(authInfo, registry, subscriptions, this, advertiser);
this.outOfBandMessageCallback = outOfBandMessageCallback;
this.subscriptions = subscriptions;
}
@Override
public synchronized HttpResponse handleRequest(HttpRequest request) throws IOException {
return doHandleRequest(request);
}
private HttpResponse doHandleRequest(HttpRequest request) throws IOException {
HttpResponse response = isUpgraded ?
httpSession.handleAuthenticatedRequest(request) : httpSession.handleRequest(request);
if (response instanceof UpgradeResponse) {
isUpgraded = true;
readKey = ((UpgradeResponse) response).getReadKey().array();
writeKey = ((UpgradeResponse) response).getWriteKey().array();
}
LOGGER.info(response.getStatusCode()+" "+request.getUri());
return response;
}
@Override
public byte[] decryptRequest(byte[] ciphertext) {
if (!isUpgraded) {
throw new RuntimeException("Cannot handle binary before connection is upgraded");
}
if (binaryProcessor == null) {
binaryProcessor = new LengthPrefixedByteArrayProcessor();
}
Collection<byte[]> res = binaryProcessor.handle(ciphertext);
if (res.isEmpty()) {
return new byte[0];
} else {
try(ByteArrayOutputStream decrypted = new ByteArrayOutputStream()) {
res.stream().map(msg -> decrypt(msg))
.forEach(bytes -> {
try {
decrypted.write(bytes);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
return decrypted.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Override
public byte[] encryptResponse(byte[] response) throws IOException {
int offset=0;
try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
while(offset < response.length) {
short length = (short) Math.min(response.length - offset, 0x400);
byte[] lengthBytes = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN)
.putShort(length).array();
baos.write(lengthBytes);
byte[] nonce = Pack.longToLittleEndian(outboundBinaryMessageCount++);
byte[] plaintext;
if (response.length == length) {
plaintext = response;
} else {
plaintext = new byte[length];
System.arraycopy(response, offset, plaintext, 0, length);
}
offset += length;
baos.write(new ChachaEncoder(writeKey, nonce).encodeCiphertext(plaintext, lengthBytes));
}
return baos.toByteArray();
}
}
private byte[] decrypt(byte[] msg) {
byte[] mac = new byte[16];
byte[] ciphertext = new byte[msg.length - 16];
System.arraycopy(msg, 0, ciphertext, 0, msg.length - 16);
System.arraycopy(msg, msg.length - 16, mac, 0, 16);
byte[] additionalData = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN)
.putShort((short) (msg.length - 16)).array();
try {
byte[] nonce = Pack.longToLittleEndian(inboundBinaryMessageCount++);
return new ChachaDecoder(readKey, nonce)
.decodeCiphertext(mac, additionalData, ciphertext);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void close() {
subscriptions.removeConnection(this);
}
@Override
public void outOfBand(HttpResponse message) {
outOfBandMessageCallback.accept(message);
}
}