1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-13 16:40:50 +02: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:
Andreas Shimokawa 2022-02-02 12:57:25 +01:00
parent 884538de5e
commit b62357dfe2
5 changed files with 182 additions and 11 deletions

View File

@ -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<>();

View File

@ -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;
}
}

View File

@ -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());

View File

@ -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);

View File

@ -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);
}
}