From 339859c8297e38d81822364b9c841a21c322c7e8 Mon Sep 17 00:00:00 2001 From: MrYoranimo Date: Fri, 5 Jan 2024 16:51:54 +0100 Subject: [PATCH] Xiaomi: change BLE max chunk size with MTU changes --- .../btle/AbstractBTLEDeviceSupport.java | 2 +- .../devices/xiaomi/XiaomiAuthService.java | 24 +---- .../devices/xiaomi/XiaomiBleSupport.java | 67 +++++++++++--- .../devices/xiaomi/XiaomiCharacteristic.java | 88 +++++++++++++++---- .../devices/xiaomi/XiaomiSppSupport.java | 9 +- 5 files changed, 138 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java index 234267cf1..7ecd3ce26 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java @@ -55,7 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBlePro public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport implements GattCallback, GattServerCallback { private static final Logger LOG = LoggerFactory.getLogger(AbstractBTLEDeviceSupport.class); - private int mMTU = 0; + private int mMTU = 23; private BtLEQueue mQueue; private Map mAvailableCharacteristics; private final Set mSupportedServices = new HashSet<>(4); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiAuthService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiAuthService.java index 3eb39478b..f1bd341ff 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiAuthService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiAuthService.java @@ -79,32 +79,16 @@ public class XiaomiAuthService extends AbstractXiaomiService { return encryptionInitialized; } - // TODO also implement for spp - protected void startEncryptedHandshake(final XiaomiBleSupport support, final nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder) { + protected void startEncryptedHandshake() { encryptionInitialized = false; - builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.AUTHENTICATING, getSupport().getContext())); - System.arraycopy(getSecretKey(getSupport().getDevice()), 0, secretKey, 0, 16); new SecureRandom().nextBytes(nonce); - support.sendCommand(builder, buildNonceCommand(nonce)); + getSupport().sendCommand("auth step 1", buildNonceCommand(nonce)); } - protected void startEncryptedHandshake(final XiaomiSppSupport support, final nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder builder) { - encryptionInitialized = false; - - builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btbr.actions.SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.AUTHENTICATING, getSupport().getContext())); - - System.arraycopy(getSecretKey(getSupport().getDevice()), 0, secretKey, 0, 16); - new SecureRandom().nextBytes(nonce); - - support.sendCommand(builder, buildNonceCommand(nonce)); - } - - protected void startClearTextHandshake(final XiaomiBleSupport support, final nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder) { - builder.add(new nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.AUTHENTICATING, getSupport().getContext())); - + protected void startClearTextHandshake() { final XiaomiProto.Auth auth = XiaomiProto.Auth.newBuilder() .setUserId(getUserId(getSupport().getDevice())) .build(); @@ -115,7 +99,7 @@ public class XiaomiAuthService extends AbstractXiaomiService { .setAuth(auth) .build(); - support.sendCommand(builder, command); + getSupport().sendCommand("auth step 1", command); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java index b53f01c93..757fae5bb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiBleSupport.java @@ -116,29 +116,54 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport { return builder; } - XiaomiBleSupport.this.characteristicCommandRead = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandRead, mXiaomiSupport.getAuthService()); - XiaomiBleSupport.this.characteristicCommandRead.setEncrypted(uuidSet.isEncrypted()); - XiaomiBleSupport.this.characteristicCommandRead.setChannelHandler(mXiaomiSupport::handleCommandBytes); - XiaomiBleSupport.this.characteristicCommandWrite = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandWrite, mXiaomiSupport.getAuthService()); - XiaomiBleSupport.this.characteristicCommandWrite.setEncrypted(uuidSet.isEncrypted()); - XiaomiBleSupport.this.characteristicActivityData = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicActivityData, mXiaomiSupport.getAuthService()); - XiaomiBleSupport.this.characteristicActivityData.setChannelHandler(mXiaomiSupport.getHealthService().getActivityFetcher()::addChunk); - XiaomiBleSupport.this.characteristicActivityData.setEncrypted(uuidSet.isEncrypted()); - XiaomiBleSupport.this.characteristicDataUpload = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicDataUpload, mXiaomiSupport.getAuthService()); - XiaomiBleSupport.this.characteristicDataUpload.setEncrypted(uuidSet.isEncrypted()); - XiaomiBleSupport.this.characteristicDataUpload.setIncrementNonce(false); + // FIXME: + // Because the first handshake packet is sent before the actions in the builder are run, + // the maximum message size is not properly initialized if the device itself does not request + // the MTU to be upgraded. However, since we will upgrade the MTU ourselves to the highest + // possible (512) and the device will (likely) respond with something higher than 247, + // we will initialize the characteristics with that MTU. + final int expectedMtu = 247; + characteristicCommandRead = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandRead, mXiaomiSupport.getAuthService()); + characteristicCommandRead.setEncrypted(uuidSet.isEncrypted()); + characteristicCommandRead.setChannelHandler(mXiaomiSupport::handleCommandBytes); + characteristicCommandRead.setMtu(expectedMtu); + characteristicCommandWrite = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicCommandWrite, mXiaomiSupport.getAuthService()); + characteristicCommandWrite.setEncrypted(uuidSet.isEncrypted()); + characteristicCommandWrite.setMtu(expectedMtu); + characteristicActivityData = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicActivityData, mXiaomiSupport.getAuthService()); + characteristicActivityData.setChannelHandler(mXiaomiSupport.getHealthService().getActivityFetcher()::addChunk); + characteristicActivityData.setEncrypted(uuidSet.isEncrypted()); + characteristicActivityData.setMtu(expectedMtu); + characteristicDataUpload = new XiaomiCharacteristic(XiaomiBleSupport.this, btCharacteristicDataUpload, mXiaomiSupport.getAuthService()); + characteristicDataUpload.setEncrypted(uuidSet.isEncrypted()); + characteristicDataUpload.setIncrementNonce(false); + characteristicDataUpload.setMtu(expectedMtu); - builder.requestMtu(247); + // request highest possible MTU; device should response with the highest supported MTU anyway + builder.requestMtu(512); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); builder.notify(btCharacteristicCommandWrite, true); builder.notify(btCharacteristicCommandRead, true); builder.notify(btCharacteristicActivityData, true); builder.notify(btCharacteristicDataUpload, true); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext())); if (uuidSet.isEncrypted()) { - mXiaomiSupport.getAuthService().startEncryptedHandshake(XiaomiBleSupport.this, builder); + builder.add(new PlainAction() { + @Override + public boolean run(BluetoothGatt gatt) { + mXiaomiSupport.getAuthService().startEncryptedHandshake(); + return true; + } + }); } else { - mXiaomiSupport.getAuthService().startClearTextHandshake(XiaomiBleSupport.this, builder); + builder.add(new PlainAction() { + @Override + public boolean run(BluetoothGatt gatt) { + mXiaomiSupport.getAuthService().startClearTextHandshake(); + return true; + } + }); } return builder; @@ -175,6 +200,20 @@ public class XiaomiBleSupport extends XiaomiConnectionSupport { public boolean getImplicitCallbackModify() { return mXiaomiSupport.getImplicitCallbackModify(); } + + @Override + public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { + super.onMtuChanged(gatt, mtu, status); + + if (characteristicCommandRead != null) + characteristicCommandRead.setMtu(mtu); + if (characteristicCommandWrite != null) + characteristicCommandWrite.setMtu(mtu); + if (characteristicDataUpload != null) + characteristicDataUpload.setMtu(mtu); + if (characteristicActivityData != null) + characteristicActivityData.setMtu(mtu); + } }; public XiaomiBleSupport(final XiaomiSupport xiaomiSupport) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java index e246fe284..8638d3c30 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java @@ -42,9 +42,6 @@ public class XiaomiCharacteristic { public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0}; - // max chunk size, including headers - public static final int MAX_WRITE_SIZE = 242; - private final XiaomiBleSupport mSupport; private final BluetoothGattCharacteristic bluetoothGattCharacteristic; @@ -56,6 +53,10 @@ public class XiaomiCharacteristic { public boolean incrementNonce = true; private int encryptedIndex = 0; + // max chunk size, including headers + private int maxWriteSize = 244; // MTU of 247 - 3 bytes for the ATT overhead (based on lowest MTU observed after increasing MTU to 512) + private int maxWriteSizeForCurrentMessage; + // Chunking private int numChunks = 0; private int currentChunk = 0; @@ -145,6 +146,17 @@ public class XiaomiCharacteristic { sendNext(builder); } + private void sendChunk(final TransactionBuilder builder, final int index, final int chunkPayloadSize) { + final byte[] payload = currentPayload.getBytesToSend(); + final int startIndex = index * chunkPayloadSize; + final int endIndex = Math.min((index + 1) * chunkPayloadSize, payload.length); + LOG.debug("Sending chunk {} from {} to {} for {}", index, startIndex, endIndex, currentPayload.getTaskName()); + final byte[] chunkToSend = new byte[2 + endIndex - startIndex]; + BLETypeConversions.writeUint16(chunkToSend, 0, index + 1); + System.arraycopy(payload, startIndex, chunkToSend, 2, endIndex - startIndex); + builder.write(bluetoothGattCharacteristic, chunkToSend); + } + public void onCharacteristicChanged(final byte[] value) { final ByteBuffer buf = ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN); @@ -199,8 +211,16 @@ public class XiaomiCharacteristic { case 1: // Chunked ack final byte subtype = buf.get(); + + final byte[] remaining = new byte[buf.remaining()]; + if (buf.hasRemaining()) { + buf.get(remaining); + LOG.debug("Operation CHUNK_ACK of type {} has additional payload: {}", + subtype, GB.hexdump(remaining)); + } + switch (subtype) { - case 0: + case 0: { LOG.debug("Got chunked ack end"); if (currentPayload != null && currentPayload.getCallback() != null) { currentPayload.getCallback().onSend(); @@ -209,23 +229,21 @@ public class XiaomiCharacteristic { sendingChunked = false; sendNext(null); return; - case 1: + } + case 1: { LOG.debug("Got chunked ack start"); final TransactionBuilder builder = mSupport.createTransactionBuilder("send chunks for " + currentPayload.getTaskName()); 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 endIndex = Math.min((i + 1) * MAX_WRITE_SIZE, payload.length); - LOG.debug("Sending chunk {} from {} to {} for {}", i, startIndex, endIndex, currentPayload.getTaskName()); - final byte[] chunkToSend = new byte[2 + endIndex - startIndex]; - BLETypeConversions.writeUint16(chunkToSend, 0, i + 1); - System.arraycopy(payload, startIndex, chunkToSend, 2, endIndex - startIndex); - builder.write(bluetoothGattCharacteristic, chunkToSend); + final int chunkPayloadSize = maxWriteSizeForCurrentMessage - 2; + + for (int i = 0; i * chunkPayloadSize < payload.length; i++) { + sendChunk(builder, i, chunkPayloadSize); } builder.queue(mSupport.getQueue()); return; - case 2: + } + case 2: { LOG.warn("Got chunked nack for {}", currentPayload.getTaskName()); if (currentPayload != null && currentPayload.getCallback() != null) { currentPayload.getCallback().onNack(); @@ -234,6 +252,35 @@ public class XiaomiCharacteristic { sendingChunked = false; sendNext(null); return; + } + case 5: { + short[] invalidChunks = new short[remaining.length / 2]; + if (remaining.length > 0) { + ByteBuffer remainingBuffer = ByteBuffer.wrap(remaining).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < remaining.length / 2; i++) { + invalidChunks[i] = remainingBuffer.getShort(); + } + + LOG.info("Got chunk request, requested chunks: {}", Arrays.toString(invalidChunks)); + final TransactionBuilder builder = mSupport.createTransactionBuilder("resend chunks for " + currentPayload.getTaskName()); + + for (short chunkIndex : invalidChunks) { + // chunk indices start at 1 + sendChunk(builder, chunkIndex - 1, maxWriteSizeForCurrentMessage - 2); + } + } else { + LOG.warn("Got chunk request, no chunk indices requested"); + + if (maxWriteSize != maxWriteSizeForCurrentMessage) { + LOG.info("MTU changed while sending message, prepending message to queue and resending"); + ((LinkedList) payloadQueue).addFirst(currentPayload); + currentPayload = null; + sendingChunked = false; + sendNext(null); + return; + } + } + } } LOG.warn("Unknown chunked ack subtype {} for {}", subtype, currentPayload.getTaskName()); @@ -276,6 +323,7 @@ public class XiaomiCharacteristic { currentPayload.getCallback().onNack(); } } + currentPayload = null; waitingAck = false; sendNext(null); @@ -306,6 +354,9 @@ public class XiaomiCharacteristic { currentPayload.setBytesToSend(authService.encrypt(currentPayload.getBytesToSend(), incrementNonce ? encryptedIndex : 0)); } + // before checking whether message should be chunked, read the maximum message size for this transaction + maxWriteSizeForCurrentMessage = maxWriteSize; + if (shouldWriteChunked(currentPayload.getBytesToSend())) { if (encrypt && incrementNonce) { // Prepend encrypted index for the nonce @@ -325,7 +376,7 @@ public class XiaomiCharacteristic { buf.putShort((short) 0); buf.put((byte) 0); buf.put((byte) (encrypt ? 1 : 0)); - buf.putShort((short) Math.ceil(currentPayload.getBytesToSend().length / (float) MAX_WRITE_SIZE)); + buf.putShort((short) Math.ceil(currentPayload.getBytesToSend().length / (float) (maxWriteSizeForCurrentMessage - 2))); final TransactionBuilder builder = b == null ? mSupport.createTransactionBuilder("send chunked start for " + currentPayload.getTaskName()) : b; builder.write(bluetoothGattCharacteristic, buf.array()); @@ -368,7 +419,7 @@ public class XiaomiCharacteristic { } // payload + 6 bytes at the start with the encryption stuff - return payload.length + 6 > MAX_WRITE_SIZE; + return payload.length + 6 > maxWriteSizeForCurrentMessage; } private void sendAck() { @@ -389,6 +440,11 @@ public class XiaomiCharacteristic { builder.queue(mSupport.getQueue()); } + public void setMtu(final int newMtu) { + // subtract ATT packet header size + maxWriteSize = newMtu - 3; + } + private static class Payload { private final String taskName; private final byte[] bytes; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java index 0c3b0e788..240964989 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java @@ -76,7 +76,14 @@ public class XiaomiSppSupport extends XiaomiConnectionSupport { } builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); - mXiaomiSupport.getAuthService().startEncryptedHandshake(XiaomiSppSupport.this, builder); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext())); + builder.add(new PlainAction() { + @Override + public boolean run(BluetoothSocket socket) { + mXiaomiSupport.getAuthService().startEncryptedHandshake(); + return true; + } + }); return builder; }