1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-29 13:26:50 +01:00

Mi Band 6: Support sending encrypted packets on new firmware

This commit is contained in:
Andreas Shimokawa 2021-09-01 00:16:50 +02:00
parent 52756a5f29
commit 117cd57463
5 changed files with 108 additions and 31 deletions

View File

@ -225,6 +225,13 @@ public class HuamiService {
public static final byte[] COMMAND_TEXT_NOTIFICATION = new byte[] {0x05, 0x01}; public static final byte[] COMMAND_TEXT_NOTIFICATION = new byte[] {0x05, 0x01};
/**
* Endpoints for 2021 chunked protocol
*
*/
public static final short CHUNKED2021_ENDPOINT_AUTH = 0x82;
public static final short CHUNKED2021_ENDPOINT_COMPAT = 0x90;
static { static {
MIBAND_DEBUG = new HashMap<>(); MIBAND_DEBUG = new HashMap<>();
MIBAND_DEBUG.put(UUID_SERVICE_MIBAND_SERVICE, "MiBand Service"); MIBAND_DEBUG.put(UUID_SERVICE_MIBAND_SERVICE, "MiBand Service");

View File

@ -135,6 +135,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationS
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.RealtimeSamplesSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.RealtimeSamplesSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs; import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
@ -184,6 +186,15 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
private static int currentButtonPressCount = 0; private static int currentButtonPressCount = 0;
private static long currentButtonPressTime = 0; private static long currentButtonPressTime = 0;
private static long currentButtonTimerActivationTime = 0; private static long currentButtonTimerActivationTime = 0;
public byte[] sharedSessionKey;
public int encryptedSequenceNr;
public byte handle;
public byte getNextHandle() {
return handle++;
}
private Timer buttonActionTimer = null; private Timer buttonActionTimer = null;
private static final Logger LOG = LoggerFactory.getLogger(HuamiSupport.class); private static final Logger LOG = LoggerFactory.getLogger(HuamiSupport.class);
@ -221,6 +232,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
private boolean heartRateNotifyEnabled; private boolean heartRateNotifyEnabled;
private int mMTU = 23; private int mMTU = 23;
protected int mActivitySampleSize = 4; protected int mActivitySampleSize = 4;
private boolean force2021Protocol = false;
public HuamiSupport() { public HuamiSupport() {
this(LOG); this(LOG);
@ -255,6 +267,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
characteristicChunked2021Write = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_WRITE); characteristicChunked2021Write = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_WRITE);
characteristicChunked2021Read = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ); characteristicChunked2021Read = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ);
if (characteristicChunked2021Write != null && GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean("force_new_protocol", false)) { if (characteristicChunked2021Write != null && GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean("force_new_protocol", false)) {
force2021Protocol = true;
new InitOperation2021(authenticate, authFlags, cryptFlags, this, builder).perform(); new InitOperation2021(authenticate, authFlags, cryptFlags, this, builder).perform();
} else { } else {
new InitOperation(authenticate, authFlags, cryptFlags, this, builder).perform(); new InitOperation(authenticate, authFlags, cryptFlags, this, builder).perform();
@ -1811,14 +1824,21 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
} }
} }
byte[] alarmMessage = new byte[] { byte[] alarmMessage = new byte[]{
(byte) 0x2, // TODO what is this? (byte) 0x2, // TODO what is this?
(byte) (actionMask | alarm.getPosition()), // action mask + alarm slot (byte) (actionMask | alarm.getPosition()), // action mask + alarm slot
(byte) calendar.get(Calendar.HOUR_OF_DAY), (byte) calendar.get(Calendar.HOUR_OF_DAY),
(byte) calendar.get(Calendar.MINUTE), (byte) calendar.get(Calendar.MINUTE),
(byte) daysMask, (byte) daysMask,
}; };
builder.write(characteristic, alarmMessage); if (force2021Protocol) {
alarmMessage = ArrayUtils.insert(0, alarmMessage, (byte) 0x01);
writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_COMPAT, getNextHandle(), alarmMessage, true);
} else {
builder.write(characteristic, alarmMessage);
}
// TODO: react on 0x10, 0x02, 0x01 on notification (success) // TODO: react on 0x10, 0x02, 0x01 on notification (success)
} }
@ -2830,22 +2850,60 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
} }
} }
public void writeToChunked2021(TransactionBuilder builder, short type, byte handle, byte[] data) { public void writeToChunked2021(TransactionBuilder builder, short type, byte handle, byte[] data, boolean encrypt) {
int remaining = data.length; int remaining = data.length;
int length = data.length;
byte count = 0; byte count = 0;
int header_size = 11; int header_size = 11;
if (encrypt) {
byte[] messagekey = new byte[16];
for (int i = 0; i < 16; i++) {
messagekey[i] = (byte) (sharedSessionKey[i] ^ handle);
}
int encrypted_length = length + 8;
int overflow = encrypted_length % 16;
if (overflow > 0) {
encrypted_length += (16 - overflow);
}
byte[] encryptable_payload = new byte[encrypted_length];
System.arraycopy(data, 0, encryptable_payload, 0, length);
encryptable_payload[length] = (byte) (encryptedSequenceNr & 0xff);
encryptable_payload[length + 1] = (byte) ((encryptedSequenceNr >> 8) & 0xff);
encryptable_payload[length + 2] = (byte) ((encryptedSequenceNr >> 16) & 0xff);
encryptable_payload[length + 3] = (byte) ((encryptedSequenceNr >> 24) & 0xff);
encryptedSequenceNr++;
int checksum = CheckSums.getCRC32(encryptable_payload, 0, length + 4);
encryptable_payload[length + 4] = (byte) (checksum & 0xff);
encryptable_payload[length + 5] = (byte) ((checksum >> 8) & 0xff);
encryptable_payload[length + 6] = (byte) ((checksum >> 16) & 0xff);
encryptable_payload[length + 7] = (byte) ((checksum >> 24) & 0xff);
remaining = encrypted_length;
try {
data = CryptoUtils.encryptAES(encryptable_payload, messagekey);
} catch (Exception e) {
LOG.error("error while encrypting", e);
return;
}
}
while (remaining > 0) { while (remaining > 0) {
int MAX_CHUNKLENGTH = mMTU - 3 - header_size; int MAX_CHUNKLENGTH = mMTU - 3 - header_size;
int copybytes = Math.min(remaining, MAX_CHUNKLENGTH); int copybytes = Math.min(remaining, MAX_CHUNKLENGTH);
byte[] chunk = new byte[copybytes + header_size]; byte[] chunk = new byte[copybytes + header_size];
byte flags = 0; byte flags = 0;
if (encrypt) {
flags |= 0x08;
}
if (count == 0) { if (count == 0) {
flags |= 0x01; flags |= 0x01;
chunk[5] = (byte) (data.length & 0xff); chunk[5] = (byte) (length & 0xff);
chunk[6] = (byte) ((data.length >> 8) & 0xff); chunk[6] = (byte) ((length >> 8) & 0xff);
chunk[7] = (byte) ((data.length >> 16) & 0xff); chunk[7] = (byte) ((length >> 16) & 0xff);
chunk[8] = (byte) ((data.length >> 24) & 0xff); chunk[8] = (byte) ((length >> 24) & 0xff);
chunk[9] = (byte) (type & 0xff); chunk[9] = (byte) (type & 0xff);
chunk[10] = (byte) ((type >> 8) & 0xff); chunk[10] = (byte) ((type >> 8) & 0xff);
} }

View File

@ -16,30 +16,21 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations; package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattCharacteristic;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class InitOperation2021 extends InitOperation { public class InitOperation2021 extends InitOperation {
@ -96,7 +87,7 @@ public class InitOperation2021 extends InitOperation {
sendPubkeyCommand[3] = 0x02; sendPubkeyCommand[3] = 0x02;
System.arraycopy(publicEC, 0, sendPubkeyCommand, 4, 48); System.arraycopy(publicEC, 0, sendPubkeyCommand, 4, 48);
//testAuth(); //testAuth();
huamiSupport.writeToChunked2021(builder, (short) 0x82, (byte) 0x66, sendPubkeyCommand); huamiSupport.writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_AUTH, huamiSupport.getNextHandle(), sendPubkeyCommand, false);
} }
private native byte[] ecdh_generate_public(byte[] privateEC); private native byte[] ecdh_generate_public(byte[] privateEC);
@ -124,7 +115,7 @@ public class InitOperation2021 extends InitOperation {
if (value.length > 1 && value[0] == 0x03) { if (value.length > 1 && value[0] == 0x03) {
int sequenceNumber = value[4]; int sequenceNumber = value[4];
int headerSize; int headerSize;
if (sequenceNumber == 0 && value[9] == (byte) 0x82 && value[10] == 0x00 && value[11] == 0x10 && value[12] == 0x04 && value[13] == 0x01) { if (sequenceNumber == 0 && value[9] == (byte) HuamiService.CHUNKED2021_ENDPOINT_AUTH && value[10] == 0x00 && value[11] == 0x10 && value[12] == 0x04 && value[13] == 0x01) {
reassembleBuffer_pointer = 0; reassembleBuffer_pointer = 0;
headerSize = 14; headerSize = 14;
reassembleBuffer_expectedBytes = value[5] - 3; reassembleBuffer_expectedBytes = value[5] - 3;
@ -135,7 +126,7 @@ public class InitOperation2021 extends InitOperation {
return false; return false;
} }
headerSize = 5; headerSize = 5;
} else if (value[9] == (byte) 0x82 && value[10] == 0x00 && value[11] == 0x10 && value[12] == 0x05 && value[13] == 0x01) { } else if (value[9] == (byte) HuamiService.CHUNKED2021_ENDPOINT_AUTH && value[10] == 0x00 && value[11] == 0x10 && value[12] == 0x05 && value[13] == 0x01) {
try { try {
TransactionBuilder builder = createTransactionBuilder("Authenticated, now initialize phase 2"); TransactionBuilder builder = createTransactionBuilder("Authenticated, now initialize phase 2");
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
@ -163,21 +154,23 @@ public class InitOperation2021 extends InitOperation {
System.arraycopy(reassembleBuffer, 0, remoteRandom, 0, 16); System.arraycopy(reassembleBuffer, 0, remoteRandom, 0, 16);
System.arraycopy(reassembleBuffer, 16, remotePublicEC, 0, 48); System.arraycopy(reassembleBuffer, 16, remotePublicEC, 0, 48);
sharedEC = ecdh_generate_shared(privateEC, remotePublicEC); sharedEC = ecdh_generate_shared(privateEC, remotePublicEC);
huamiSupport.encryptedSequenceNr = ((sharedEC[0] & 0xff) | ((sharedEC[1] & 0xff) << 8) | ((sharedEC[2] & 0xff) << 16) | ((sharedEC[3] & 0xff) << 24));
byte[] secretKey = getSecretKey(); byte[] secretKey = getSecretKey();
for (int i = 0; i < 16; i++) { for (int i = 0; i < 16; i++) {
finalSharedSessionAES[i] = (byte) (sharedEC[i + 8] ^ secretKey[i]); finalSharedSessionAES[i] = (byte) (sharedEC[i + 8] ^ secretKey[i]);
} }
huamiSupport.sharedSessionKey = finalSharedSessionAES;
try { try {
byte[] encryptedRandom1 = encryptAES(remoteRandom, secretKey); byte[] encryptedRandom1 = CryptoUtils.encryptAES(remoteRandom, secretKey);
byte[] encryptedRandom2 = encryptAES(remoteRandom, finalSharedSessionAES); byte[] encryptedRandom2 = CryptoUtils.encryptAES(remoteRandom, finalSharedSessionAES);
if (encryptedRandom1.length == 16 && encryptedRandom2.length == 16) { if (encryptedRandom1.length == 16 && encryptedRandom2.length == 16) {
byte[] command = new byte[33]; byte[] command = new byte[33];
command[0] = 0x05; command[0] = 0x05;
System.arraycopy(encryptedRandom1, 0, command, 1, 16); System.arraycopy(encryptedRandom1, 0, command, 1, 16);
System.arraycopy(encryptedRandom2, 0, command, 17, 16); System.arraycopy(encryptedRandom2, 0, command, 17, 16);
TransactionBuilder builder = createTransactionBuilder("Sending double encryted random to device"); TransactionBuilder builder = createTransactionBuilder("Sending double encryted random to device");
huamiSupport.writeToChunked2021(builder, (short) 0x82, (byte) 0x67, command); huamiSupport.writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_AUTH, huamiSupport.getNextHandle(), command, false);
huamiSupport.performImmediately(builder); huamiSupport.performImmediately(builder);
} }
} catch (Exception e) { } catch (Exception e) {
@ -195,12 +188,4 @@ public class InitOperation2021 extends InitOperation {
} }
} }
private byte[] encryptAES(byte[] value, byte[] secretKey) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException {
byte[] mValue = Arrays.copyOfRange(value, 0, 16);
@SuppressLint("GetInstance") Cipher ecipher = Cipher.getInstance("AES/ECB/NoPadding");
SecretKeySpec newKey = new SecretKeySpec(secretKey, "AES");
ecipher.init(Cipher.ENCRYPT_MODE, newKey);
return ecipher.doFinal(mValue);
}
} }

View File

@ -64,6 +64,12 @@ public class CheckSums {
return (int) (crc.getValue()); return (int) (crc.getValue());
} }
public static int getCRC32(byte[] seq,int offset, int length) {
CRC32 crc = new CRC32();
crc.update(seq,offset,length);
return (int) (crc.getValue());
}
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
if (args == null || args.length == 0) { if (args == null || args.length == 0) {
throw new IllegalArgumentException("Pass the files to be checksummed as arguments"); throw new IllegalArgumentException("Pass the files to be checksummed as arguments");

View File

@ -0,0 +1,21 @@
package nodomain.freeyourgadget.gadgetbridge.util;
import android.annotation.SuppressLint;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
public class CryptoUtils {
public static byte[] encryptAES(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.ENCRYPT_MODE, newKey);
return ecipher.doFinal(value);
}
}