mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
Xiaomi: Refactor XiaomiCharacteristic to improve logging and ordering
Fixes a potential race condition on initialization, since the chunked commands were being scheduled on a separate transaction builder, which would be scheduled to be written before the initialization.
This commit is contained in:
parent
1acd3ac5fd
commit
48e149aefe
@ -66,6 +66,8 @@ public class XiaomiAuthService extends AbstractXiaomiService {
|
|||||||
public static final int CMD_NONCE = 26;
|
public static final int CMD_NONCE = 26;
|
||||||
public static final int CMD_AUTH = 27;
|
public static final int CMD_AUTH = 27;
|
||||||
|
|
||||||
|
private boolean encryptionInitialized = false;
|
||||||
|
|
||||||
private final byte[] secretKey = new byte[16];
|
private final byte[] secretKey = new byte[16];
|
||||||
private final byte[] nonce = new byte[16];
|
private final byte[] nonce = new byte[16];
|
||||||
private final byte[] encryptionKey = new byte[16];
|
private final byte[] encryptionKey = new byte[16];
|
||||||
@ -77,17 +79,19 @@ public class XiaomiAuthService extends AbstractXiaomiService {
|
|||||||
super(support);
|
super(support);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isEncryptionInitialized() {
|
||||||
|
return encryptionInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
protected void startEncryptedHandshake(final TransactionBuilder builder) {
|
protected void startEncryptedHandshake(final TransactionBuilder builder) {
|
||||||
|
encryptionInitialized = false;
|
||||||
|
|
||||||
builder.add(new SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.AUTHENTICATING, getSupport().getContext()));
|
builder.add(new SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.AUTHENTICATING, getSupport().getContext()));
|
||||||
|
|
||||||
System.arraycopy(getSecretKey(getSupport().getDevice()), 0, secretKey, 0, 16);
|
System.arraycopy(getSecretKey(getSupport().getDevice()), 0, secretKey, 0, 16);
|
||||||
new SecureRandom().nextBytes(nonce);
|
new SecureRandom().nextBytes(nonce);
|
||||||
|
|
||||||
// TODO use sendCommand
|
getSupport().sendCommand(builder, buildNonceCommand(nonce));
|
||||||
builder.write(
|
|
||||||
getSupport().getCharacteristic(XiaomiEncryptedSupport.UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE),
|
|
||||||
ArrayUtils.addAll(PAYLOAD_HEADER_AUTH, buildNonceCommand(nonce))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void startClearTextHandshake(final TransactionBuilder builder, String userId) {
|
protected void startClearTextHandshake(final TransactionBuilder builder, String userId) {
|
||||||
@ -103,7 +107,7 @@ public class XiaomiAuthService extends AbstractXiaomiService {
|
|||||||
.setAuth(auth)
|
.setAuth(auth)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
getSupport().sendCommand("start clear text handshake", command);
|
getSupport().sendCommand(builder, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -138,6 +142,8 @@ public class XiaomiAuthService extends AbstractXiaomiService {
|
|||||||
if (cmd.getSubtype() == CMD_AUTH || cmd.getAuth().getStatus() == 1) {
|
if (cmd.getSubtype() == CMD_AUTH || cmd.getAuth().getStatus() == 1) {
|
||||||
LOG.info("Authenticated!");
|
LOG.info("Authenticated!");
|
||||||
|
|
||||||
|
encryptionInitialized = cmd.getSubtype() == CMD_AUTH;
|
||||||
|
|
||||||
final TransactionBuilder builder = getSupport().createTransactionBuilder("phase 2 initialize");
|
final TransactionBuilder builder = getSupport().createTransactionBuilder("phase 2 initialize");
|
||||||
builder.add(new SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.INITIALIZED, getSupport().getContext()));
|
builder.add(new SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.INITIALIZED, getSupport().getContext()));
|
||||||
getSupport().phase2Initialize();
|
getSupport().phase2Initialize();
|
||||||
@ -227,7 +233,7 @@ public class XiaomiAuthService extends AbstractXiaomiService {
|
|||||||
return cmd.setAuth(auth.build()).build();
|
return cmd.setAuth(auth.build()).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] buildNonceCommand(final byte[] nonce) {
|
public static XiaomiProto.Command buildNonceCommand(final byte[] nonce) {
|
||||||
final XiaomiProto.PhoneNonce.Builder phoneNonce = XiaomiProto.PhoneNonce.newBuilder();
|
final XiaomiProto.PhoneNonce.Builder phoneNonce = XiaomiProto.PhoneNonce.newBuilder();
|
||||||
phoneNonce.setNonce(ByteString.copyFrom(nonce));
|
phoneNonce.setNonce(ByteString.copyFrom(nonce));
|
||||||
|
|
||||||
@ -238,7 +244,7 @@ public class XiaomiAuthService extends AbstractXiaomiService {
|
|||||||
command.setType(COMMAND_TYPE);
|
command.setType(COMMAND_TYPE);
|
||||||
command.setSubtype(CMD_NONCE);
|
command.setSubtype(CMD_NONCE);
|
||||||
command.setAuth(auth.build());
|
command.setAuth(auth.build());
|
||||||
return command.build().toByteArray();
|
return command.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] computeAuthStep3Hmac(final byte[] secretKey,
|
public static byte[] computeAuthStep3Hmac(final byte[] secretKey,
|
||||||
|
@ -50,8 +50,8 @@ public class XiaomiCharacteristic {
|
|||||||
private final UUID characteristicUUID;
|
private final UUID characteristicUUID;
|
||||||
|
|
||||||
// Encryption
|
// Encryption
|
||||||
private XiaomiAuthService authService = null;
|
private final XiaomiAuthService authService;
|
||||||
private boolean isEncrypted = false;
|
private boolean isEncrypted;
|
||||||
private short encryptedIndex = 0;
|
private short encryptedIndex = 0;
|
||||||
|
|
||||||
// Chunking
|
// Chunking
|
||||||
@ -61,10 +61,10 @@ public class XiaomiCharacteristic {
|
|||||||
|
|
||||||
// Scheduling
|
// Scheduling
|
||||||
// TODO timeouts
|
// TODO timeouts
|
||||||
private final Queue<byte[]> payloadQueue = new LinkedList<>();
|
private final Queue<Payload> payloadQueue = new LinkedList<>();
|
||||||
private boolean waitingAck = false;
|
private boolean waitingAck = false;
|
||||||
private boolean sendingChunked = false;
|
private boolean sendingChunked = false;
|
||||||
private byte[] currentSending = null;
|
private Payload currentPayload = null;
|
||||||
|
|
||||||
private Handler handler = null;
|
private Handler handler = null;
|
||||||
|
|
||||||
@ -99,30 +99,35 @@ public class XiaomiCharacteristic {
|
|||||||
this.payloadQueue.clear();
|
this.payloadQueue.clear();
|
||||||
this.waitingAck = false;
|
this.waitingAck = false;
|
||||||
this.sendingChunked = false;
|
this.sendingChunked = false;
|
||||||
this.currentSending = null;
|
this.currentPayload = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write bytes to this characteristic, encrypting and splitting it into chunks if necessary.
|
* Write bytes to this characteristic, encrypting and splitting it into chunks if necessary.
|
||||||
*/
|
*/
|
||||||
public void write(final byte[] value) {
|
public void write(final String taskName, final byte[] value) {
|
||||||
payloadQueue.add(value);
|
write(null, new Payload(taskName, value));
|
||||||
sendNext();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write bytes to this characteristic directly.
|
* Write bytes to this characteristic, encrypting and splitting it into chunks if necessary. Uses
|
||||||
|
* the provided if we need to schedule something, otherwise it will be queued as other commands.
|
||||||
*/
|
*/
|
||||||
public void writeDirect(final TransactionBuilder builder, final byte[] value) {
|
public void write(final TransactionBuilder builder, final byte[] value) {
|
||||||
builder.write(bluetoothGattCharacteristic, value);
|
write(builder, new Payload(builder.getTaskName(), value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void write(final TransactionBuilder builder, final Payload payload) {
|
||||||
|
payloadQueue.add(payload);
|
||||||
|
sendNext(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onCharacteristicChanged(final byte[] value) {
|
public void onCharacteristicChanged(final byte[] value) {
|
||||||
if (Arrays.equals(value, PAYLOAD_ACK)) {
|
if (Arrays.equals(value, PAYLOAD_ACK)) {
|
||||||
LOG.debug("Got ack");
|
LOG.debug("Got ack");
|
||||||
currentSending = null;
|
currentPayload = null;
|
||||||
waitingAck = false;
|
waitingAck = false;
|
||||||
sendNext();
|
sendNext(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,34 +183,35 @@ public class XiaomiCharacteristic {
|
|||||||
switch (subtype) {
|
switch (subtype) {
|
||||||
case 0:
|
case 0:
|
||||||
LOG.debug("Got chunked ack end");
|
LOG.debug("Got chunked ack end");
|
||||||
currentSending = null;
|
currentPayload = null;
|
||||||
sendingChunked = false;
|
sendingChunked = false;
|
||||||
sendNext();
|
sendNext(null);
|
||||||
return;
|
return;
|
||||||
case 1:
|
case 1:
|
||||||
LOG.debug("Got chunked ack start");
|
LOG.debug("Got chunked ack start");
|
||||||
final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunks");
|
final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunks for " + currentPayload.getTaskName());
|
||||||
for (int i = 0; i * MAX_WRITE_SIZE < currentSending.length; i++) {
|
final byte[] payload = currentPayload.getBytesToSend();
|
||||||
|
for (int i = 0; i * MAX_WRITE_SIZE < payload.length; i++) {
|
||||||
final int startIndex = i * MAX_WRITE_SIZE;
|
final int startIndex = i * MAX_WRITE_SIZE;
|
||||||
final int endIndex = Math.min((i + 1) * MAX_WRITE_SIZE, currentSending.length);
|
final int endIndex = Math.min((i + 1) * MAX_WRITE_SIZE, payload.length);
|
||||||
LOG.debug("Sending chunk {} from {} to {}", i, startIndex, endIndex);
|
LOG.debug("Sending chunk {} from {} to {} for {}", i, startIndex, endIndex, currentPayload.getTaskName());
|
||||||
final byte[] chunkToSend = new byte[2 + endIndex - startIndex];
|
final byte[] chunkToSend = new byte[2 + endIndex - startIndex];
|
||||||
BLETypeConversions.writeUint16(chunkToSend, 0, i + 1);
|
BLETypeConversions.writeUint16(chunkToSend, 0, i + 1);
|
||||||
System.arraycopy(currentSending, startIndex, chunkToSend, 2, endIndex - startIndex);
|
System.arraycopy(payload, startIndex, chunkToSend, 2, endIndex - startIndex);
|
||||||
builder.write(bluetoothGattCharacteristic, chunkToSend);
|
builder.write(bluetoothGattCharacteristic, chunkToSend);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.queue(mSupport.getQueue());
|
builder.queue(mSupport.getQueue());
|
||||||
return;
|
return;
|
||||||
case 2:
|
case 2:
|
||||||
LOG.warn("Got chunked nack");
|
LOG.warn("Got chunked nack for {}", currentPayload.getTaskName());
|
||||||
currentSending = null;
|
currentPayload = null;
|
||||||
sendingChunked = false;
|
sendingChunked = false;
|
||||||
sendNext();
|
sendNext(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.warn("Unknown chunked ack subtype {}", subtype);
|
LOG.warn("Unknown chunked ack subtype {} for {}", subtype, currentPayload.getTaskName());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
case 2:
|
case 2:
|
||||||
@ -233,66 +239,72 @@ public class XiaomiCharacteristic {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendNext() {
|
private void sendNext(@Nullable final TransactionBuilder b) {
|
||||||
if (waitingAck || sendingChunked) {
|
if (waitingAck || sendingChunked) {
|
||||||
LOG.debug("Already sending something");
|
LOG.debug("Already sending something");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final byte[] payload = payloadQueue.poll();
|
currentPayload = payloadQueue.poll();
|
||||||
if (payload == null) {
|
if (currentPayload == null) {
|
||||||
LOG.debug("Nothing to send");
|
LOG.debug("Nothing to send");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEncrypted) {
|
final boolean encrypt = isEncrypted && authService.isEncryptionInitialized();
|
||||||
currentSending = authService.encrypt(payload, encryptedIndex);
|
|
||||||
} else {
|
if (encrypt) {
|
||||||
currentSending = payload;
|
currentPayload.setBytesToSend(authService.encrypt(currentPayload.getBytesToSend(), encryptedIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldWriteChunked(currentSending)) {
|
if (shouldWriteChunked(currentPayload.getBytesToSend())) {
|
||||||
if (isEncrypted) {
|
if (encrypt) {
|
||||||
// Prepend encrypted index for the nonce
|
// Prepend encrypted index for the nonce
|
||||||
currentSending = ByteBuffer.allocate(2 + currentSending.length).order(ByteOrder.LITTLE_ENDIAN)
|
currentPayload.setBytesToSend(
|
||||||
.putShort(encryptedIndex++)
|
ByteBuffer.allocate(2 + currentPayload.getBytesToSend().length).order(ByteOrder.LITTLE_ENDIAN)
|
||||||
.put(currentSending)
|
.putShort(encryptedIndex++)
|
||||||
.array();
|
.put(currentPayload.getBytesToSend())
|
||||||
|
.array()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.debug("Sending next - chunked");
|
LOG.debug("Sending {} - chunked", currentPayload.getTaskName());
|
||||||
|
|
||||||
sendingChunked = true;
|
sendingChunked = true;
|
||||||
|
|
||||||
final ByteBuffer buf = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN);
|
final ByteBuffer buf = ByteBuffer.allocate(6).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
buf.putShort((short) 0);
|
buf.putShort((short) 0);
|
||||||
buf.put((byte) 0);
|
buf.put((byte) 0);
|
||||||
buf.put((byte) (isEncrypted ? 1 : 0));
|
buf.put((byte) (encrypt ? 1 : 0));
|
||||||
buf.putShort((short) Math.ceil(currentSending.length / (float) MAX_WRITE_SIZE));
|
buf.putShort((short) Math.ceil(currentPayload.getBytesToSend().length / (float) MAX_WRITE_SIZE));
|
||||||
|
|
||||||
final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunked start");
|
final TransactionBuilder builder = b == null ? mSupport.createTransactionBuilder("send chunked start for " + currentPayload.getTaskName()) : b;
|
||||||
builder.write(bluetoothGattCharacteristic, buf.array());
|
builder.write(bluetoothGattCharacteristic, buf.array());
|
||||||
builder.queue(mSupport.getQueue());
|
if (b == null) {
|
||||||
|
builder.queue(mSupport.getQueue());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG.debug("Sending next - single");
|
LOG.debug("Sending {} - single", currentPayload.getTaskName());
|
||||||
|
|
||||||
// Encrypt single command
|
// Encrypt single command
|
||||||
final int commandLength = (isEncrypted ? 6 : 4) + currentSending.length;
|
final int commandLength = (encrypt ? 6 : 4) + currentPayload.getBytesToSend().length;
|
||||||
|
|
||||||
final ByteBuffer buf = ByteBuffer.allocate(commandLength).order(ByteOrder.LITTLE_ENDIAN);
|
final ByteBuffer buf = ByteBuffer.allocate(commandLength).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
buf.putShort((short) 0);
|
buf.putShort((short) 0);
|
||||||
buf.put((byte) 2); // 2 for command
|
buf.put((byte) 2); // 2 for command
|
||||||
buf.put((byte) (isEncrypted ? 1 : 2));
|
buf.put((byte) (encrypt ? 1 : 2));
|
||||||
if (isEncrypted) {
|
if (encrypt) {
|
||||||
buf.putShort(encryptedIndex++);
|
buf.putShort(encryptedIndex++);
|
||||||
}
|
}
|
||||||
buf.put(currentSending); // it's already encrypted
|
buf.put(currentPayload.getBytesToSend()); // it's already encrypted
|
||||||
|
|
||||||
waitingAck = true;
|
waitingAck = true;
|
||||||
|
|
||||||
final TransactionBuilder builder = mSupport.createTransactionBuilder("send single command");
|
final TransactionBuilder builder = b == null ? mSupport.createTransactionBuilder("send single command for " + currentPayload.getTaskName()) : b;
|
||||||
builder.write(bluetoothGattCharacteristic, buf.array());
|
builder.write(bluetoothGattCharacteristic, buf.array());
|
||||||
builder.queue(mSupport.getQueue());
|
if (b == null) {
|
||||||
|
builder.queue(mSupport.getQueue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,4 +339,29 @@ public class XiaomiCharacteristic {
|
|||||||
public interface Handler {
|
public interface Handler {
|
||||||
void handle(final byte[] payload);
|
void handle(final byte[] payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class Payload {
|
||||||
|
private final String taskName;
|
||||||
|
private final byte[] bytes;
|
||||||
|
|
||||||
|
// Bytes that will actually be sent (might be encrypted)
|
||||||
|
private byte[] bytesToSend;
|
||||||
|
|
||||||
|
public Payload(final String taskName, final byte[] bytes) {
|
||||||
|
this.taskName = taskName;
|
||||||
|
this.bytes = bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTaskName() {
|
||||||
|
return taskName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBytesToSend(final byte[] bytesToSend) {
|
||||||
|
this.bytesToSend = bytesToSend;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBytesToSend() {
|
||||||
|
return bytesToSend != null ? bytesToSend : bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -415,10 +415,22 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME builder is ignored
|
this.characteristicCommandWrite.write(taskName, command.toByteArray());
|
||||||
final byte[] commandBytes = command.toByteArray();
|
}
|
||||||
LOG.debug("Sending command {}", GB.hexdump(commandBytes));
|
|
||||||
this.characteristicCommandWrite.write(commandBytes);
|
/**
|
||||||
|
* Realistically, this function should only be used during auth, as we must schedule the command after
|
||||||
|
* notifications were enabled on the characteristics, and for that we need the builder to guarantee the
|
||||||
|
* order.
|
||||||
|
*/
|
||||||
|
public void sendCommand(final TransactionBuilder builder, final XiaomiProto.Command command) {
|
||||||
|
if (this.characteristicCommandWrite == null) {
|
||||||
|
// Can sometimes happen in race conditions when connecting + receiving calendar event or weather updates
|
||||||
|
LOG.warn("characteristicCommandWrite is null!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.characteristicCommandWrite.write(builder, command.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendCommand(final String taskName, final int type, final int subtype) {
|
public void sendCommand(final String taskName, final int type, final int subtype) {
|
||||||
|
Loading…
Reference in New Issue
Block a user