diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java index 93f1a65b4..696dae035 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiService.java @@ -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"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index 1b3040fd9..501cf4be9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -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); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation2021.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation2021.java index 04d5ce1aa..a42ff6483 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation2021.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation2021.java @@ -16,30 +16,21 @@ along with this program. If not, see . */ 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); - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java index e9ffab909..b47599bcb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java @@ -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"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java new file mode 100644 index 000000000..b4f7be56d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java @@ -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); + } +}