mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-01 14:32:54 +01:00
Mi Band 6: Support sending encrypted packets on new firmware
This commit is contained in:
parent
52756a5f29
commit
117cd57463
@ -225,6 +225,13 @@ public class HuamiService {
|
||||
|
||||
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 {
|
||||
MIBAND_DEBUG = new HashMap<>();
|
||||
MIBAND_DEBUG.put(UUID_SERVICE_MIBAND_SERVICE, "MiBand Service");
|
||||
|
@ -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.serial.GBDeviceProtocol;
|
||||
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.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
@ -184,6 +186,15 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
private static int currentButtonPressCount = 0;
|
||||
private static long currentButtonPressTime = 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 static final Logger LOG = LoggerFactory.getLogger(HuamiSupport.class);
|
||||
@ -221,6 +232,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
private boolean heartRateNotifyEnabled;
|
||||
private int mMTU = 23;
|
||||
protected int mActivitySampleSize = 4;
|
||||
private boolean force2021Protocol = false;
|
||||
|
||||
public HuamiSupport() {
|
||||
this(LOG);
|
||||
@ -255,6 +267,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
characteristicChunked2021Write = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_WRITE);
|
||||
characteristicChunked2021Read = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ);
|
||||
if (characteristicChunked2021Write != null && GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean("force_new_protocol", false)) {
|
||||
force2021Protocol = true;
|
||||
new InitOperation2021(authenticate, authFlags, cryptFlags, this, builder).perform();
|
||||
} else {
|
||||
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) (actionMask | alarm.getPosition()), // action mask + alarm slot
|
||||
(byte) calendar.get(Calendar.HOUR_OF_DAY),
|
||||
(byte) calendar.get(Calendar.MINUTE),
|
||||
(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)
|
||||
}
|
||||
|
||||
@ -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 length = data.length;
|
||||
byte count = 0;
|
||||
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) {
|
||||
int MAX_CHUNKLENGTH = mMTU - 3 - header_size;
|
||||
int copybytes = Math.min(remaining, MAX_CHUNKLENGTH);
|
||||
byte[] chunk = new byte[copybytes + header_size];
|
||||
|
||||
byte flags = 0;
|
||||
if (encrypt) {
|
||||
flags |= 0x08;
|
||||
}
|
||||
if (count == 0) {
|
||||
flags |= 0x01;
|
||||
chunk[5] = (byte) (data.length & 0xff);
|
||||
chunk[6] = (byte) ((data.length >> 8) & 0xff);
|
||||
chunk[7] = (byte) ((data.length >> 16) & 0xff);
|
||||
chunk[8] = (byte) ((data.length >> 24) & 0xff);
|
||||
chunk[5] = (byte) (length & 0xff);
|
||||
chunk[6] = (byte) ((length >> 8) & 0xff);
|
||||
chunk[7] = (byte) ((length >> 16) & 0xff);
|
||||
chunk[8] = (byte) ((length >> 24) & 0xff);
|
||||
chunk[9] = (byte) (type & 0xff);
|
||||
chunk[10] = (byte) ((type >> 8) & 0xff);
|
||||
}
|
||||
|
@ -16,30 +16,21 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
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.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class InitOperation2021 extends InitOperation {
|
||||
@ -96,7 +87,7 @@ public class InitOperation2021 extends InitOperation {
|
||||
sendPubkeyCommand[3] = 0x02;
|
||||
System.arraycopy(publicEC, 0, sendPubkeyCommand, 4, 48);
|
||||
//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);
|
||||
@ -124,7 +115,7 @@ public class InitOperation2021 extends InitOperation {
|
||||
if (value.length > 1 && value[0] == 0x03) {
|
||||
int sequenceNumber = value[4];
|
||||
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;
|
||||
headerSize = 14;
|
||||
reassembleBuffer_expectedBytes = value[5] - 3;
|
||||
@ -135,7 +126,7 @@ public class InitOperation2021 extends InitOperation {
|
||||
return false;
|
||||
}
|
||||
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 {
|
||||
TransactionBuilder builder = createTransactionBuilder("Authenticated, now initialize phase 2");
|
||||
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, 16, remotePublicEC, 0, 48);
|
||||
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();
|
||||
for (int i = 0; i < 16; i++) {
|
||||
finalSharedSessionAES[i] = (byte) (sharedEC[i + 8] ^ secretKey[i]);
|
||||
}
|
||||
huamiSupport.sharedSessionKey = finalSharedSessionAES;
|
||||
try {
|
||||
byte[] encryptedRandom1 = encryptAES(remoteRandom, secretKey);
|
||||
byte[] encryptedRandom2 = encryptAES(remoteRandom, finalSharedSessionAES);
|
||||
byte[] encryptedRandom1 = CryptoUtils.encryptAES(remoteRandom, secretKey);
|
||||
byte[] encryptedRandom2 = CryptoUtils.encryptAES(remoteRandom, finalSharedSessionAES);
|
||||
if (encryptedRandom1.length == 16 && encryptedRandom2.length == 16) {
|
||||
byte[] command = new byte[33];
|
||||
command[0] = 0x05;
|
||||
System.arraycopy(encryptedRandom1, 0, command, 1, 16);
|
||||
System.arraycopy(encryptedRandom2, 0, command, 17, 16);
|
||||
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);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,12 @@ public class CheckSums {
|
||||
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 {
|
||||
if (args == null || args.length == 0) {
|
||||
throw new IllegalArgumentException("Pass the files to be checksummed as arguments");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user