mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-01-01 05:25:50 +01:00
Mi Band 6: Sync alarms set on the watch like on Amazfit Bip U and others
This adds code to decrypt chunked protocol replies for configuration data. Also some (disabled) code for SMS reply.
This commit is contained in:
parent
884538de5e
commit
b62357dfe2
@ -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<>();
|
||||
|
@ -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 <http://www.gnu.org/licenses/>. */
|
||||
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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user