diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java index 72c7acefb..35fc46c9e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java @@ -229,10 +229,10 @@ public class HuamiService { /** * Endpoints for 2021 chunked protocol - * */ - public static final short CHUNKED2021_ENDPOINT_AUTH = 0x82; - public static final short CHUNKED2021_ENDPOINT_COMPAT = 0x90; + public static final short CHUNKED2021_ENDPOINT_AUTH = 0x0082; + public static final short CHUNKED2021_ENDPOINT_COMPAT = 0x0090; + public static final short CHUNKED2021_ENDPOINT_SMSREPLY = 0x0013; static { MIBAND_DEBUG = new HashMap<>(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiChunked2021Decoder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiChunked2021Decoder.java new file mode 100644 index 000000000..002943eaa --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiChunked2021Decoder.java @@ -0,0 +1,147 @@ +/* Copyright (C) 2022 Andreas Shimokawa + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; + +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class HuamiChunked2021Decoder { + private static final Logger LOG = LoggerFactory.getLogger(HuamiChunked2021Decoder.class); + private Byte currentHandle; + private int currentType; + private int currentLength; + ByteBuffer reassemblyBuffer; + private final HuamiSupport huamiSupport; + + public HuamiChunked2021Decoder(HuamiSupport huamiSupport) { + this.huamiSupport = huamiSupport; + } + + + public byte[] decode(byte[] data) { + int i = 0; + if (data[i++] != 0x03) { + return null; + } + boolean encrypted = false; + byte flags = data[i++]; + if ((flags & 0x08) == 0x08) { + encrypted = true; + } + if (huamiSupport.force2021Protocol) { + i++; // skip extended header + } + byte handle = data[i++]; + if (currentHandle != null && currentHandle != handle) { + LOG.warn("ignoring handle " + handle + ", expected " + currentHandle); + return null; + } + byte count = data[i++]; + if ((flags & 0x01) == 0x01) { // beginning + int full_length = (data[i++] & 0xff) | ((data[i++] & 0xff) << 8) | ((data[i++] & 0xff) << 16) | ((data[i++] & 0xff) << 24); + currentLength = full_length; + if (encrypted) { + int encrypted_length = full_length + 8; + int overflow = encrypted_length % 16; + if (overflow > 0) { + encrypted_length += (16 - overflow); + } + full_length = encrypted_length; + } + reassemblyBuffer = ByteBuffer.allocate(full_length); + currentType = (data[i++] & 0xff) | ((data[i++] & 0xff) << 8); + currentHandle = handle; + } + reassemblyBuffer.put(data, i, data.length - i); + if ((flags & 0x02) == 0x02) { // end + byte[] buf = reassemblyBuffer.array(); + if (encrypted) { + byte[] messagekey = new byte[16]; + for (int j = 0; j < 16; j++) { + messagekey[j] = (byte) (huamiSupport.sharedSessionKey[j] ^ handle); + } + try { + buf = CryptoUtils.decryptAES(buf, messagekey); + buf = ArrayUtils.subarray(buf, 0, currentLength); + LOG.info("decrypted data: " + GB.hexdump(buf)); + } catch (Exception e) { + LOG.warn("error decrypting " + e); + return null; + } + } + if (currentType == HuamiService.CHUNKED2021_ENDPOINT_COMPAT) { + LOG.info("got configuration data"); + currentHandle = null; + currentType = 0; + return ArrayUtils.remove(buf, 0); + } + if (currentType == HuamiService.CHUNKED2021_ENDPOINT_SMSREPLY && false) { // unsafe for now, disabled, also we shoud return somehing and then parse in HuamiSupport instead of firing stuff here + LOG.debug("got command for SMS reply"); + if (buf[0] == 0x0d) { + try { + TransactionBuilder builder = huamiSupport.performInitialized("allow sms reply"); + huamiSupport.writeToChunked2021(builder, (short) 0x0013, huamiSupport.getNextHandle(), new byte[]{(byte) 0x0e, 0x01}, huamiSupport.force2021Protocol, false); + builder.queue(huamiSupport.getQueue()); + } catch (IOException e) { + LOG.error("Unable to allow sms reply"); + } + } else if (buf[0] == 0x0b) { + String phoneNumber = null; + String smsReply = null; + for (i = 1; i < buf.length; i++) { + if (buf[i] == 0) { + phoneNumber = new String(buf, 1, i - 1); + // there are four unknown bytes between caller and reply + smsReply = new String(buf, i + 5, buf.length - i - 6); + break; + } + } + if (phoneNumber != null && !phoneNumber.isEmpty()) { + LOG.debug("will send message '" + smsReply + "' to number '" + phoneNumber + "'"); + GBDeviceEventNotificationControl devEvtNotificationControl = new GBDeviceEventNotificationControl(); + devEvtNotificationControl.handle = -1; + devEvtNotificationControl.phoneNumber = phoneNumber; + devEvtNotificationControl.reply = smsReply; + devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY; + huamiSupport.evaluateGBDeviceEvent(devEvtNotificationControl); + try { + TransactionBuilder builder = huamiSupport.performInitialized("ack sms reply"); + byte[] ackSentCommand = new byte[]{0x0c, 0x01}; + huamiSupport.writeToChunked2021(builder, (short) 0x0013, huamiSupport.getNextHandle(), ackSentCommand, huamiSupport.force2021Protocol, false); + builder.queue(huamiSupport.getQueue()); + } catch (IOException e) { + LOG.error("Unable to ack sms reply"); + } + } + } + } + currentHandle = null; + currentType = 0; + } + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index 8bda94803..ef15030a3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -234,7 +234,8 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { private boolean heartRateNotifyEnabled; private int mMTU = 23; protected int mActivitySampleSize = 4; - private boolean force2021Protocol = false; + protected boolean force2021Protocol = false; + private HuamiChunked2021Decoder huamiChunked2021Decoder; public HuamiSupport() { this(LOG); @@ -266,8 +267,11 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { heartRateNotifyEnabled = false; boolean authenticate = needsAuth && (cryptFlags == 0x00); needsAuth = false; - characteristicChunked2021Write = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_WRITE); characteristicChunked2021Read = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ); + if (characteristicChunked2021Read != null) { + huamiChunked2021Decoder = new HuamiChunked2021Decoder(this); + } + characteristicChunked2021Write = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_WRITE); if (characteristicChunked2021Write != null && GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean("force_new_protocol", false)) { force2021Protocol = true; new InitOperation2021(authenticate, authFlags, cryptFlags, this, builder).perform(); @@ -386,6 +390,9 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUDIO), enable); builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUDIODATA), enable); builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT), enable); + if (characteristicChunked2021Read != null) { + builder.notify(characteristicChunked2021Read, enable); + } return this; } @@ -1007,18 +1014,23 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { if (cannedMessagesSpec.type == CannedMessagesSpec.TYPE_REJECTEDCALLS) { try { TransactionBuilder builder = performInitialized("Set canned messages"); - int handle = 0x12345678; + + for (int i = 0; i < 16; i++) { + byte[] delete_command = new byte[]{0x07, (byte) (handle & 0xff), (byte) ((handle & 0xff00) >> 8), (byte) ((handle & 0xff0000) >> 16), (byte) ((handle & 0xff000000) >> 24)}; + writeToChunked2021(builder, (short) 0x0013, getNextHandle(), delete_command, force2021Protocol, false); + handle++; + } + handle = 0x12345678; for (String cannedMessage : cannedMessagesSpec.cannedMessages) { - int length = cannedMessage.getBytes().length + 5; + int length = cannedMessage.getBytes().length + 6; ByteBuffer buf = ByteBuffer.allocate(length); buf.order(ByteOrder.LITTLE_ENDIAN); - buf.put((byte) 0x05); // create buf.putInt(handle++); buf.put(cannedMessage.getBytes()); - - writeToChunked2021(builder, (short) 0x0013, getNextHandle(), buf.array(), false, false); + buf.put((byte) 0x00); + writeToChunked2021(builder, (short) 0x0013, getNextHandle(), buf.array(), force2021Protocol, false); } builder.queue(getQueue()); } catch (IOException ex) { @@ -1706,6 +1718,12 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { } else if (HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION.equals(characteristicUUID)) { handleConfigurationInfo(characteristic.getValue()); return true; + } else if (HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ.equals(characteristicUUID) && huamiChunked2021Decoder != null) { + byte[] decoded_data = huamiChunked2021Decoder.decode(characteristic.getValue()); + if (decoded_data != null) { + handleConfigurationInfo(decoded_data); + } + return true; } else { LOG.info("Unhandled characteristic changed: " + characteristicUUID); logMessageContent(characteristic.getValue()); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband6/MiBand6Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband6/MiBand6Support.java index 94ef9e231..ed3e30291 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband6/MiBand6Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/miband6/MiBand6Support.java @@ -31,7 +31,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband5.MiBand5Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2020; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperationNew; public class MiBand6Support extends MiBand5Support { private static final Logger LOG = LoggerFactory.getLogger(MiBand6Support.class); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java index b4f7be56d..a841ac640 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java @@ -18,4 +18,11 @@ public class CryptoUtils { ecipher.init(Cipher.ENCRYPT_MODE, newKey); return ecipher.doFinal(value); } + + public static byte[] decryptAES(byte[] value, byte[] secretKey) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException { + @SuppressLint("GetInstance") Cipher ecipher = Cipher.getInstance("AES/ECB/NoPadding"); + SecretKeySpec newKey = new SecretKeySpec(secretKey, "AES"); + ecipher.init(Cipher.DECRYPT_MODE, newKey); + return ecipher.doFinal(value); + } }