From ccdb843b6e2054875369dd88b347051bb1c2eac5 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Tue, 20 Sep 2016 23:09:42 +0200 Subject: [PATCH] Improved Mi Band 2 support #323 - connecting works and is stable - firmware and hardware version is displayed - time is set --- .../devices/miband/MiBand2Service.java | 43 +++++- .../btle/AbstractBTLEDeviceSupport.java | 17 ++- .../gadgetbridge/service/btle/BtLEQueue.java | 18 +++ .../devices/miband/MiBand2Support.java | 135 ++++------------- .../miband2/operations/InitOperation.java | 138 ++++++++++++++++++ 5 files changed, 239 insertions(+), 112 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/InitOperation.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java index 0c6f4f6f..0ab8b897 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java @@ -23,7 +23,7 @@ public class MiBand2Service { public static final UUID UUID_UNKNOWN_CHARACTERISTIC7 = UUID.fromString("00000007-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC8 = UUID.fromString("00000008-0000-3512-2118-0009af100700"); // service uuid fee1 - public static final UUID UUID_UNKNOWN_CHARACTERISTIC9 = UUID.fromString("00000009-0000-3512-2118-0009af100700"); + public static final UUID UUID_CHARACTERISTIC_AUTH = UUID.fromString("00000009-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC10 = UUID.fromString("00000010-0000-3512-2118-0009af100700"); // set metric distance @@ -233,6 +233,47 @@ public class MiBand2Service { private static final Map MIBAND_DEBUG; + /** + * Mi Band 2 authentication has three steps. + * This is step 1: sending a "secret" key to the band. + * This is byte 0, followed by {@link #AUTH_BYTE} and then the key. + * In the response, it is byte 1 in the byte[] value. + */ + public static final byte AUTH_SEND_KEY = 0x01; + /** + * Mi Band 2 authentication has three steps. + * This is step 2: requesting a random authentication key from the band. + * This is byte 0, followed by {@link #AUTH_BYTE}. + * In the response, it is byte 1 in the byte[] value. + */ + public static final byte AUTH_REQUEST_RANDOM_AUTH_NUMBER = 0x02; + /** + * Mi Band 2 authentication has three steps. + * This is step 3: sending the encrypted random authentication key to the band. + * This is byte 0, followed by {@link #AUTH_BYTE} and then the encrypted random authentication key. + * In the response, it is byte 1 in the byte[] value. + */ + public static final byte AUTH_SEND_ENCRYPTED_AUTH_NUMBER = 0x03; + + /** + * Received in response to any authentication requests (byte 0 in the byte[] value. + */ + public static final byte AUTH_RESPONSE = 0x10; + /** + * Receeived in response to any authentication requests (byte 2 in the byte[] value. + * 0x01 means success. + */ + public static final byte AUTH_SUCCESS = 0x01; + /** + * Received in response to any authentication requests (byte 2 in the byte[] value. + * 0x04 means failure. + */ + public static final byte AUTH_FAIL = 0x04; + /** + * In some logs it's 0x0... + */ + public static final byte AUTH_BYTE = 0x8; + static { MIBAND_DEBUG = new HashMap<>(); MIBAND_DEBUG.put(UUID_SERVICE_MIBAND_SERVICE, "MiBand Service"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java index 87f3aad0..c65262c2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/AbstractBTLEDeviceSupport.java @@ -76,7 +76,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im } } - protected TransactionBuilder createTransactionBuilder(String taskName) { + public TransactionBuilder createTransactionBuilder(String taskName) { return new TransactionBuilder(taskName); } @@ -110,7 +110,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im * @throws IOException * @see {@link #performInitialized(String)} */ - protected void performConnected(Transaction transaction) throws IOException { + public void performConnected(Transaction transaction) throws IOException { if (!isConnected()) { if (!connect()) { throw new IOException("2: Unable to connect to device: " + getDevice()); @@ -119,6 +119,19 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im getQueue().add(transaction); } + /** + * Performs the actions of the given transaction as soon as possible, + * that is, before any other queued transactions, but after the actions + * of the currently executing transaction. + * @param builder + */ + public void performImmediately(TransactionBuilder builder) throws IOException { + if (!isConnected()) { + throw new IOException("Not connected to device: " + getDevice()); + } + getQueue().insert(builder.getTransaction()); + } + public BtLEQueue getQueue() { return mQueue; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java index 21c9a3ae..49038370 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java @@ -14,6 +14,7 @@ import android.support.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.BlockingQueue; @@ -258,6 +259,23 @@ public final class BtLEQueue { } } + /** + * Adds a transaction to the beginning of the queue. + * Note that actions of the *currently executing* transaction + * will still be executed before the given transaction. + * + * @param transaction + */ + public void insert(Transaction transaction) { + LOG.debug("about to insert: " + transaction); + if (!transaction.isEmpty()) { + List tail = new ArrayList<>(mTransactions.size() + 2); + mTransactions.drainTo(tail); + mTransactions.add(transaction); + mTransactions.addAll(tail); + } + } + public void clear() { mTransactions.clear(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java index 4c372b97..84824f44 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java @@ -22,9 +22,6 @@ import java.util.GregorianCalendar; import java.util.List; import java.util.UUID; -import javax.crypto.Cipher; -import javax.crypto.spec.SecretKeySpec; - import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; @@ -57,8 +54,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactio import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ConditionalWriteAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction; -import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.InitOperation; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -90,29 +87,19 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(MiBand2Support.class); private final DeviceInfoProfile deviceInfoProfile; - private final BatteryInfoProfile batteryInfoProfile; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String s = intent.getAction(); if (s.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) { handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); - - } else if (s.equals(BatteryInfoProfile.ACTION_BATTERY_INFO)) { - handleBatteryInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO)); - } else if (s.equals(DeviceService.ACTION_MIBAND2_AUTH)) { - byte[] response = intent.getExtras().getByteArray(DeviceService.EXTRA_MIBAND2_AUTH_BYTE); - BluetoothGattCharacteristic temp = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC9); - temp.setValue(response); - mBluetoothGatt.writeCharacteristic(temp); - } } }; + private boolean needsAuth; private volatile boolean telephoneRinging; private volatile boolean isLocatingDevice; - private BluetoothGatt mBluetoothGatt; private DeviceInfo mDeviceInfo; @@ -131,14 +118,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { addSupportedService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE); deviceInfoProfile = new DeviceInfoProfile<>(this); - batteryInfoProfile = new BatteryInfoProfile<>(this); addSupportedProfile(deviceInfoProfile); - addSupportedProfile(batteryInfoProfile); LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(DeviceInfoProfile.ACTION_DEVICE_INFO); - intentFilter.addAction(BatteryInfoProfile.ACTION_BATTERY_INFO); intentFilter.addAction(DeviceService.ACTION_MIBAND2_AUTH); broadcastManager.registerReceiver(mReceiver, intentFilter); } @@ -152,15 +136,21 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { - builder.add(new SetDeviceStateAction(getDevice(), State.INITIALIZING, getContext())); - enableNotifications(builder, true) - .setLowLatency(builder) - .readDate(builder) // without reading the data, we get sporadic connection problems, especially directly after turning on BT + try { + boolean authenticate = needsAuth; + needsAuth = false; + new InitOperation(authenticate, this, builder).perform(); + } catch (IOException e) { + GB.toast(getContext(), "Initializing Mi Band 2 failed", Toast.LENGTH_SHORT, GB.ERROR, e); + } + +// builder.add(new SetDeviceStateAction(getDevice(), State.INITIALIZING, getContext())); +// enableNotifications(builder, true) +// .setLowLatency(builder) +// .readDate(builder) // without reading the data, we get sporadic connection problems, especially directly after turning on BT // this is apparently not needed anymore, and actually causes problems when bonding is not used/does not work // so we simply not use the UUID_PAIR characteristic. // .pair(builder) - .testInit(builder) - .setInitialized(builder); //.requestDeviceInfo(builder) //.requestBatteryInfo(builder); // .sendUserInfo(builder) @@ -176,27 +166,13 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { return builder; } - private MiBand2Support testInit(TransactionBuilder builder) { - //builder.read(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC6)); // example read value: 0f6200e0070804072b2c20e00708040625372064 - //builder.read(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC7)); // example read value: 0019000000 - setCurrentTimeWithService(builder); - // write key to miband2 - //builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC9), new byte[] { 0x01, 0x08, (byte) 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45 }); - // get random auth number - builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC9), new byte[] { 0x02 , 0x08}); - //builder.read(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC6)); // probably superfluous - //builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC8), new byte[] { 0x20, 0x00, 0x00, 0x02 }); - - return this; - } - // private MiBand2Support maybeAuth(TransactionBuilder builder) { // builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0), new byte[] {0x20, 0x00}); // builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0), new byte[] {0x03,0x00,(byte)0x8e,(byte)0xce,0x5a,0x09,(byte)0xb3,(byte)0xd8,0x55,0x57,0x10,0x2a,(byte)0xed,0x7d,0x6b,0x78,(byte)0xc5,(byte)0xd2}); // return this; // } - private MiBand2Support setCurrentTimeWithService(TransactionBuilder builder) { + public MiBand2Support setCurrentTimeWithService(TransactionBuilder builder) { GregorianCalendar now = BLETypeConversions.createCalendar(); byte[] bytes = BLETypeConversions.calendarToRawBytes(now, true); byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(now.getTimeZone()) }; // 0 = adjust reason bitflags? or DST offset?? , timezone @@ -241,17 +217,17 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { * * @param builder */ - private void setInitialized(TransactionBuilder builder) { + public void setInitialized(TransactionBuilder builder) { builder.add(new SetDeviceStateAction(getDevice(), State.INITIALIZED, getContext())); } // MB2: AVL // TODO: tear down the notifications on quit - private MiBand2Support enableNotifications(TransactionBuilder builder, boolean enable) { + public MiBand2Support enableNotifications(TransactionBuilder builder, boolean enable) { builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_NOTIFICATION), enable); builder.notify(getCharacteristic(GattService.UUID_SERVICE_CURRENT_TIME), enable); // Notify CHARACTERISTIC9 to receive random auth code - builder.notify(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC9), enable); + builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), enable); return this; } @@ -276,6 +252,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { @Override public void pair() { + needsAuth = true; for (int i = 0; i < 5; i++) { if (connect()) { return; @@ -365,13 +342,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { return this; } - private MiBand2Support requestBatteryInfo(TransactionBuilder builder) { - LOG.debug("Requesting Battery Info!"); - batteryInfoProfile.requestBatteryInfo(builder); - return this; - } - - private MiBand2Support requestDeviceInfo(TransactionBuilder builder) { + public MiBand2Support requestDeviceInfo(TransactionBuilder builder) { LOG.debug("Requesting Device Info!"); deviceInfoProfile.requestDeviceInfo(builder); return this; @@ -402,27 +373,6 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { return this; }*/ - /** - * Part of device initialization process. Do not call manually. - * - * @param transaction - * @return - */ - private MiBand2Support pair(TransactionBuilder transaction) { - LOG.info("Attempting to pair MI device..."); - //BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_PAIR); - // write key to miband2 - BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC9); - if (characteristic != null) { - transaction.write(characteristic, new byte[] { 0x01, 0x08, (byte) 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45 }); - //transaction.write(characteristic, new byte[] { 0x02, 0x08 }); - LOG.info("Pair write"); - } else { - LOG.info("Unable to pair MI device -- characteristic not available"); - } - return this; - } - /** * Part of device initialization process. Do not call manually. * @@ -887,7 +837,6 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { super.onCharacteristicChanged(gatt, characteristic); - mBluetoothGatt = gatt; UUID characteristicUUID = characteristic.getUuid(); if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) { @@ -908,18 +857,10 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { // } else if (MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0.equals(characteristicUUID)) { // handleUnknownCharacteristic(characteristic.getValue()); // return true; - } else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC9.equals(characteristicUUID)) { + } else if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { + LOG.info("AUTHENTICATION?? " + characteristicUUID); logMessageContent(characteristic.getValue()); - if (characteristic.getValue()[0] == 0x10 && - characteristic.getValue()[1] == 0x02 && - characteristic.getValue()[2] == 0x01) { - byte[] eValue = handleAESAuth(characteristic.getValue()); - byte[] responseValue = org.apache.commons.lang3.ArrayUtils.addAll(new byte[] {0x03, 0x08}, eValue); - Intent intent = new Intent(DeviceService.ACTION_MIBAND2_AUTH) - .putExtra(DeviceService.EXTRA_MIBAND2_AUTH_BYTE, responseValue); - LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); - } - // + return true; } else { LOG.info("Unhandled characteristic changed: " + characteristicUUID); logMessageContent(characteristic.getValue()); @@ -972,7 +913,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { } else if (MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT.equals(characteristicUUID)) { handleControlPointResult(characteristic.getValue(), status); return true; - } else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC9.equals(characteristicUUID)) { + } else if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { LOG.info("KEY AES SEND"); logMessageContent(characteristic.getValue()); return true; @@ -1039,22 +980,6 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); } - private byte[] handleAESAuth(byte[] value) { - byte[] mValue = Arrays.copyOfRange(value, 3, 19); - try { - Cipher ecipher = null; - byte[] sRandom = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45}; - ecipher = Cipher.getInstance("AES/ECB/NoPadding"); - SecretKeySpec newKey = new SecretKeySpec(sRandom, "AES"); - ecipher.init(Cipher.ENCRYPT_MODE, newKey); - byte[] enc = ecipher.doFinal(mValue); - return enc; - } catch (Exception e) { - e.printStackTrace(); - } - return value; - } - /** * React to unsolicited messages sent by the Mi Band to the MiBandService.UUID_CHARACTERISTIC_NOTIFICATION * characteristic, @@ -1185,19 +1110,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { // } LOG.warn("Device info: " + info); versionCmd.hwVersion = info.getHardwareRevision(); - versionCmd.fwVersion = info.getFirmwareRevision(); +// versionCmd.fwVersion = info.getFirmwareRevision(); // always null + versionCmd.fwVersion = info.getSoftwareRevision(); handleGBDeviceEvent(versionCmd); } - private void handleBatteryInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo info) { - batteryCmd.level = (short) info.getPercentCharged(); -// batteryCmd.state = info.getState(); -// batteryCmd.lastChargeTime = info.getLastChargeTime(); -// batteryCmd.numCharges = info.getNumCharges(); - handleGBDeviceEvent(batteryCmd); - } - - private void handleBatteryInfo(byte[] value, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { BatteryInfo info = new BatteryInfo(value); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/InitOperation.java new file mode 100644 index 00000000..7e8335db --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/InitOperation.java @@ -0,0 +1,138 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +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.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class InitOperation extends AbstractBTLEOperation { + private static final Logger LOG = LoggerFactory.getLogger(InitOperation.class); + + private final TransactionBuilder builder; + private final boolean needsAuth; + + public InitOperation(boolean needsAuth, MiBand2Support support, TransactionBuilder builder) { + super(support); + this.needsAuth = needsAuth; + this.builder = builder; + builder.setGattCallback(this); + } + + @Override + protected void doPerform() throws IOException { + getSupport().enableNotifications(builder, true); + if (needsAuth) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext())); + // write key to miband2 + byte[] sendKey = org.apache.commons.lang3.ArrayUtils.addAll(new byte[]{MiBand2Service.AUTH_SEND_KEY, MiBand2Service.AUTH_BYTE}, getSecretKey()); + builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), sendKey); + } else { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + // get random auth number + builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), requestAuthNumber()); + } + } + + private byte[] requestAuthNumber() { + return new byte[]{MiBand2Service.AUTH_REQUEST_RANDOM_AUTH_NUMBER, MiBand2Service.AUTH_BYTE}; + } + + private byte[] getSecretKey() { + return new byte[]{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45}; + } + + @Override + public TransactionBuilder performInitialized(String taskName) throws IOException { + throw new UnsupportedOperationException("This IS the initialization class, you cannot call this method"); + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { + try { + byte[] value = characteristic.getValue(); + getSupport().logMessageContent(value); + if (value[0] == MiBand2Service.AUTH_RESPONSE && + value[1] == MiBand2Service.AUTH_SEND_KEY && + value[2] == MiBand2Service.AUTH_SUCCESS) { + TransactionBuilder builder = createTransactionBuilder("Sending the secret key to the band"); + builder.write(characteristic, requestAuthNumber()); + getSupport().performImmediately(builder); + } else if (value[0] == MiBand2Service.AUTH_RESPONSE && + value[1] == MiBand2Service.AUTH_REQUEST_RANDOM_AUTH_NUMBER && + value[2] == MiBand2Service.AUTH_SUCCESS) { + // md5?? + byte[] eValue = handleAESAuth(value, getSecretKey()); + byte[] responseValue = org.apache.commons.lang3.ArrayUtils.addAll( + new byte[]{MiBand2Service.AUTH_SEND_ENCRYPTED_AUTH_NUMBER, MiBand2Service.AUTH_BYTE}, eValue); + + TransactionBuilder builder = createTransactionBuilder("Sending the encrypted random key to the band"); + builder.write(characteristic, responseValue); + getSupport().setCurrentTimeWithService(builder); + getSupport().performImmediately(builder); + } else if (value[0] == MiBand2Service.AUTH_RESPONSE && + value[1] == MiBand2Service.AUTH_SEND_ENCRYPTED_AUTH_NUMBER && + value[2] == MiBand2Service.AUTH_SUCCESS) { + TransactionBuilder builder = createTransactionBuilder("Sending the encrypted random key to the band"); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + getSupport().requestDeviceInfo(builder); + getSupport().setInitialized(builder); + getSupport().performImmediately(builder); + } else { + return super.onCharacteristicChanged(gatt, characteristic); + } + } catch (Exception e) { + GB.toast(getContext(), "Error authenticating Mi Band 2", Toast.LENGTH_LONG, GB.ERROR, e); + } + return true; + } else { + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + return super.onCharacteristicChanged(gatt, characteristic); + } + } + + private TransactionBuilder createTransactionBuilder(String task) { + TransactionBuilder builder = getSupport().createTransactionBuilder(task); + builder.setGattCallback(this); + return builder; + } + + private byte[] getMD5(byte[] message) throws NoSuchAlgorithmException { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return md5.digest(message); + } + + private byte[] handleAESAuth(byte[] value, byte[] secretKey) throws InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException { + byte[] mValue = Arrays.copyOfRange(value, 3, 19); + Cipher ecipher = Cipher.getInstance("AES/ECB/NoPadding"); + SecretKeySpec newKey = new SecretKeySpec(secretKey, "AES"); + ecipher.init(Cipher.ENCRYPT_MODE, newKey); + byte[] enc = ecipher.doFinal(mValue); + return enc; + } +}