From 732cbd9b160cd6a16e4870ff95707579da53635a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Fri, 4 Apr 2025 22:59:10 +0100 Subject: [PATCH] Zepp OS: Extract authentication to standalone service Replicate the logic from the InitOperation2021, but this makes it reusable for btrfcomm devices, without refactoring BLE devices previous to Zepp OS. --- .../service/devices/huami/HuamiSupport.java | 15 +- .../operations/init/InitOperation2021.java | 76 +++---- .../devices/huami/zeppos/ZeppOsSupport.java | 38 ++++ .../services/ZeppOsAuthenticationService.java | 187 ++++++++++++++++++ 4 files changed, 257 insertions(+), 59 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsAuthenticationService.java 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 6f795b3086..12734a3b2a 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 @@ -320,8 +320,8 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements protected BluetoothGattCharacteristic characteristicHRControlPoint; private BluetoothGattCharacteristic characteristicChunked; - private BluetoothGattCharacteristic characteristicChunked2021Write; - private BluetoothGattCharacteristic characteristicChunked2021Read; + protected BluetoothGattCharacteristic characteristicChunked2021Write; + protected BluetoothGattCharacteristic characteristicChunked2021Read; private boolean needsAuth; private volatile boolean telephoneRinging; @@ -335,7 +335,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements protected boolean isMusicAppStarted = false; protected MediaManager mediaManager; private boolean heartRateNotifyEnabled; - private static final int MIN_MTU = 23; + protected static final int MIN_MTU = 23; private int mMTU = MIN_MTU; // Keep track of the previous MTU before reconnection, so that we can request it after reconnection private int previousMtu = -1; @@ -373,7 +373,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements public void setContext(final GBDevice gbDevice, final BluetoothAdapter btAdapter, final Context context) { super.setContext(gbDevice, btAdapter, context); this.mediaManager = new MediaManager(context); - + this.sleepAsAndroidSender = new SleepAsAndroidSender(gbDevice); } @Override @@ -408,9 +408,6 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements } else { new InitOperation(authenticate, authFlags, cryptFlags, this, builder).perform(); } - if (sleepAsAndroidSender == null) { - sleepAsAndroidSender = new SleepAsAndroidSender(gbDevice); - } characteristicHRControlPoint = getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT); characteristicChunked = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER); } catch (IOException e) { @@ -4103,11 +4100,11 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements public void writeToChunked2021(final String taskName, short type, byte[] data, boolean encrypt) { try { - final TransactionBuilder builder = performInitialized(taskName); + final TransactionBuilder builder = createTransactionBuilder(taskName); writeToChunked2021(builder, type, data, encrypt); builder.queue(getQueue()); } catch (final Exception e) { - LOG.error("Failed to " + taskName, e); + LOG.error("Failed to {}", taskName, e); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/init/InitOperation2021.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/init/InitOperation2021.java index 771d0f9989..921eb11bbd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/init/InitOperation2021.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/init/InitOperation2021.java @@ -26,10 +26,10 @@ import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Objects; import java.util.Random; import java.util.UUID; -import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service; @@ -46,12 +46,8 @@ import nodomain.freeyourgadget.gadgetbridge.util.ECDH_B163; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class InitOperation2021 extends InitOperation implements Huami2021Handler { - private byte[] privateEC = new byte[24]; + private final byte[] privateEC = new byte[24]; private byte[] publicEC; - private byte[] remotePublicEC = new byte[48]; - private final byte[] remoteRandom = new byte[16]; - private byte[] sharedEC; - private final byte[] finalSharedSessionAES = new byte[16]; private final Huami2021ChunkedEncoder huami2021ChunkedEncoder; private final Huami2021ChunkedDecoder huami2021ChunkedDecoder; @@ -71,60 +67,36 @@ public class InitOperation2021 extends InitOperation implements Huami2021Handler this.huami2021ChunkedDecoder.setHuami2021Handler(this); } - private void testAuth() { - byte[] secretKey = getSecretKey(); - privateEC = new byte[]{0x0b, 0x42, (byte) 0xb9, (byte) 0xe6, 0x1c, 0x23, 0x34, 0x0e, 0x35, (byte) 0xc1, 0x6e, 0x2e, 0x7d, (byte) 0xe4, 0x33, (byte) 0xf4, (byte) 0xb5, (byte) 0x85, (byte) 0x9a, 0x72, (byte) 0xec, 0x11, 0x40, 0x27}; - remotePublicEC = new byte[]{(byte) 0xe6, 0x01, 0x6a, (byte) 0xba, 0x1d, (byte) 0xe7, (byte) 0xac, 0x0f, 0x0c, 0x7f, 0x0f, (byte) 0xf7, (byte) 0xe2, 0x24, 0x3e, 0x66, 0x62, (byte) 0xb5, (byte) 0xe0, 0x3b, 0x01, 0x00, 0x00, 0x00, (byte) 0xad, (byte) 0x8a, 0x4b, (byte) 0xed, (byte) 0xc7, 0x6a, 0x1e, (byte) 0xfd, (byte) 0xe7, 0x72, 0x5c, (byte) 0xc6, 0x62, (byte) 0xb5, 0x48, 0x35, 0x51, 0x3e, 0x3d, 0x57, 0x05, 0x00, 0x00, 0x00}; - - publicEC = ECDH_B163.ecdh_generate_public(privateEC); - sharedEC = ECDH_B163.ecdh_generate_shared(privateEC, remotePublicEC); - LOG.warn("publicEC: " + GB.hexdump(publicEC)); - LOG.warn("privateEC: " + GB.hexdump(privateEC)); - LOG.warn("remotepubEC: " + GB.hexdump(remotePublicEC)); - LOG.warn("sharedEC: " + GB.hexdump(sharedEC)); - for (int i = 0; i < 16; i++) { - finalSharedSessionAES[i] = (byte) (sharedEC[i + 8] ^ secretKey[i]); - } - LOG.warn("finalSharedAES: " + GB.hexdump(finalSharedSessionAES)); - } - @Override protected void doPerform() { huamiSupport.enableNotifications(builder, true); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); // get random auth number generateKeyPair(); - byte[] sendPubkeyCommand = new byte[48 + 4]; - sendPubkeyCommand[0] = 0x04; - sendPubkeyCommand[1] = 0x02; - sendPubkeyCommand[2] = 0x00; - sendPubkeyCommand[3] = 0x02; - System.arraycopy(publicEC, 0, sendPubkeyCommand, 4, 48); - //testAuth(); - huami2021ChunkedEncoder.write(builder, Huami2021Service.CHUNKED2021_ENDPOINT_AUTH, sendPubkeyCommand, true, false); + final byte[] sendPubKeyCommand = new byte[48 + 4]; + sendPubKeyCommand[0] = 0x04; + sendPubKeyCommand[1] = 0x02; + sendPubKeyCommand[2] = 0x00; + sendPubKeyCommand[3] = 0x02; + System.arraycopy(publicEC, 0, sendPubKeyCommand, 4, 48); + huami2021ChunkedEncoder.write(builder, Huami2021Service.CHUNKED2021_ENDPOINT_AUTH, sendPubKeyCommand, true, false); } private void generateKeyPair() { - Random r = new Random(); - r.nextBytes(privateEC); + new Random().nextBytes(privateEC); publicEC = ECDH_B163.ecdh_generate_public(privateEC); } - @Override - public TransactionBuilder performInitialized(String taskName) { - 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(); + final UUID characteristicUUID = characteristic.getUuid(); if (!HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ.equals(characteristicUUID)) { - LOG.info("Unhandled characteristic changed: " + characteristicUUID); + LOG.warn("Unhandled characteristic changed: {}", characteristicUUID); return super.onCharacteristicChanged(gatt, characteristic); } - byte[] value = characteristic.getValue(); + final byte[] value = characteristic.getValue(); if (value.length <= 1 || value[0] != 0x03) { // Not chunked return super.onCharacteristicChanged(gatt, characteristic); @@ -149,12 +121,16 @@ public class InitOperation2021 extends InitOperation implements Huami2021Handler LOG.debug("Got remote random + public key"); // Received remote random (16 bytes) + public key (48 bytes) + final byte[] remotePublicEC = new byte[48]; + final byte[] remoteRandom = new byte[16]; + final byte[] finalSharedSessionAES = new byte[16]; + System.arraycopy(payload, 3, remoteRandom, 0, 16); System.arraycopy(payload, 19, remotePublicEC, 0, 48); - sharedEC = ECDH_B163.ecdh_generate_shared(privateEC, remotePublicEC); - int encryptedSequenceNumber = (sharedEC[0] & 0xff) | ((sharedEC[1] & 0xff) << 8) | ((sharedEC[2] & 0xff) << 16) | ((sharedEC[3] & 0xff) << 24); + final byte[] sharedEC = Objects.requireNonNull(ECDH_B163.ecdh_generate_shared(privateEC, remotePublicEC)); + final int encryptedSequenceNumber = (sharedEC[0] & 0xff) | ((sharedEC[1] & 0xff) << 8) | ((sharedEC[2] & 0xff) << 16) | ((sharedEC[3] & 0xff) << 24); - byte[] secretKey = getSecretKey(); + final byte[] secretKey = getSecretKey(); for (int i = 0; i < 16; i++) { finalSharedSessionAES[i] = (byte) (sharedEC[i + 8] ^ secretKey[i]); } @@ -164,10 +140,10 @@ public class InitOperation2021 extends InitOperation implements Huami2021Handler huami2021ChunkedDecoder.setEncryptionParameters(finalSharedSessionAES); try { - byte[] encryptedRandom1 = CryptoUtils.encryptAES(remoteRandom, secretKey); - byte[] encryptedRandom2 = CryptoUtils.encryptAES(remoteRandom, finalSharedSessionAES); + final byte[] encryptedRandom1 = CryptoUtils.encryptAES(remoteRandom, secretKey); + final byte[] encryptedRandom2 = CryptoUtils.encryptAES(remoteRandom, finalSharedSessionAES); if (encryptedRandom1.length == 16 && encryptedRandom2.length == 16) { - byte[] command = new byte[33]; + final byte[] command = new byte[33]; command[0] = 0x05; System.arraycopy(encryptedRandom1, 0, command, 1, 16); System.arraycopy(encryptedRandom2, 0, command, 17, 16); @@ -175,14 +151,14 @@ public class InitOperation2021 extends InitOperation implements Huami2021Handler huami2021ChunkedEncoder.write(builder, Huami2021Service.CHUNKED2021_ENDPOINT_AUTH, command, true, false); huamiSupport.performImmediately(builder); } - } catch (Exception e) { + } catch (final Exception e) { LOG.error("AES encryption failed", e); } } else if (payload[0] == RESPONSE && payload[1] == 0x05 && payload[2] == SUCCESS) { LOG.debug("Auth Success"); try { - TransactionBuilder builder = createTransactionBuilder("Authenticated, now initialize phase 2"); + final TransactionBuilder builder = createTransactionBuilder("Authenticated, now initialize phase 2"); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); builder.setCallback(null); // remove init operation as the callback huamiSupport.enableFurtherNotifications(builder, true); @@ -192,7 +168,7 @@ public class InitOperation2021 extends InitOperation implements Huami2021Handler huamiSupport.phase3Initialize(builder); huamiSupport.setInitialized(builder); huamiSupport.performImmediately(builder); - } catch (Exception e) { + } catch (final Exception e) { LOG.error("failed initializing device", e); } } else if (payload[0] == RESPONSE && payload[1] == 0x05 && payload[2] == 0x25) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsSupport.java index 700ffefe03..c5576317f5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsSupport.java @@ -85,6 +85,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsMapsInstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsMusicInstallHandler; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.SleepAsAndroidSender; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsCoordinator; @@ -115,6 +116,9 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021ChunkedDecoder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021ChunkedEncoder; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiPhoneGpsStatus; @@ -129,6 +133,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlarmsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAssistantService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAppsService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAuthenticationService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCalendarService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCannedMessagesService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsDisplayItemsService; @@ -171,6 +176,7 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer // Services private final ZeppOsServicesService servicesService = new ZeppOsServicesService(this); + private final ZeppOsAuthenticationService authenticationService = new ZeppOsAuthenticationService(this); private final ZeppOsFileTransferService fileTransferService = new ZeppOsFileTransferService(this); private final ZeppOsConfigService configService = new ZeppOsConfigService(this); private final ZeppOsAgpsService agpsService = new ZeppOsAgpsService(this); @@ -205,6 +211,7 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer private final Set mIsEncrypted = new HashSet<>(); private final Map mServiceMap = new LinkedHashMap() {{ put(servicesService.getEndpoint(), servicesService); + put(authenticationService.getEndpoint(), authenticationService); put(fileTransferService.getEndpoint(), fileTransferService); put(configService.getEndpoint(), configService); put(agpsService.getEndpoint(), agpsService); @@ -274,6 +281,31 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer super.dispose(); } + @Override + protected TransactionBuilder initializeDevice(final TransactionBuilder builder) { + characteristicChunked2021Read = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_READ); + if (characteristicChunked2021Read != null && huami2021ChunkedDecoder == null) { + huami2021ChunkedDecoder = new Huami2021ChunkedDecoder(this, force2021Protocol()); + } + + characteristicChunked2021Write = getCharacteristic(HuamiService.UUID_CHARACTERISTIC_CHUNKEDTRANSFER_2021_WRITE); + if (characteristicChunked2021Write != null && huami2021ChunkedEncoder == null) { + huami2021ChunkedEncoder = new Huami2021ChunkedEncoder(characteristicChunked2021Write, force2021Protocol(), getMTU()); + } + + if (characteristicChunked2021Write == null || characteristicChunked2021Read == null) { + LOG.warn("Chunked 2021 characteristics are null, will attempt to reconnect"); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.WAITING_FOR_RECONNECT, getContext())); + return builder; + } + + builder.notify(characteristicChunked2021Read, true); + + authenticationService.startAuthentication(builder); + + return builder; + } + @Override public void onSendConfiguration(final String config) { final Prefs prefs = getDevicePrefs(); @@ -1157,6 +1189,7 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer @Override public void phase2Initialize(final TransactionBuilder builder) { LOG.info("2021 phase2Initialize..."); + setCurrentTimeWithService(builder); requestMTU(builder); requestBatteryInfo(builder); @@ -1384,6 +1417,11 @@ public class ZeppOsSupport extends HuamiSupport implements ZeppOsFileTransferSer } } + public void setEncryptionParameters(final int encryptedSequenceNr, final byte[] sharedSessionKey) { + huami2021ChunkedEncoder.setEncryptionParameters(encryptedSequenceNr, sharedSessionKey); + huami2021ChunkedDecoder.setEncryptionParameters(sharedSessionKey); + } + protected void handle2021Workout(final byte[] payload) { switch (payload[0]) { case WORKOUT_CMD_APP_OPEN: diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsAuthenticationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsAuthenticationService.java new file mode 100644 index 0000000000..f52a10c211 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsAuthenticationService.java @@ -0,0 +1,187 @@ +/* Copyright (C) 2025 José Rebelo + + 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services; + +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.Random; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport; +import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils; +import nodomain.freeyourgadget.gadgetbridge.util.ECDH_B163; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +/** + * This is a reimplementation of nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.init.InitOperation2021 + * to avoid a big refactor on older devices, while making it compatible with btrfcomm devices. + */ +public class ZeppOsAuthenticationService extends AbstractZeppOsService { + private static final Logger LOG = LoggerFactory.getLogger(ZeppOsAuthenticationService.class); + + public static final short ENDPOINT = 0x0082; + + public static final byte CMD_PUB_KEY = 0x04; + public static final byte CMD_SESSION_KEY = 0x05; + + private final byte[] privateEC = new byte[24]; + private final byte[] publicEC = new byte[48]; + private final byte[] finalSharedSessionAES = new byte[16]; + + public ZeppOsAuthenticationService(final ZeppOsSupport support) { + super(support, false); + } + + @Override + public short getEndpoint() { + return ENDPOINT; + } + + @Override + public void handlePayload(final byte[] payload) { + if (payload[0] != HuamiService.RESPONSE) { + LOG.warn("Got non-response auth byte {}", String.format("0x%02x", payload[0])); + return; + } + + switch (payload[1]) { + case CMD_PUB_KEY: + if (payload[2] != HuamiService.SUCCESS) { + LOG.error("Got pub key command failure, status={}", String.format("0x%02x", payload[0])); + return; + } + + LOG.debug("Got remote random + public key"); + final byte[] remoteRandom = new byte[16]; + final byte[] remotePublicEC = new byte[48]; + + // Received remote random (16 bytes) + public key (48 bytes) + System.arraycopy(payload, 3, remoteRandom, 0, 16); + System.arraycopy(payload, 19, remotePublicEC, 0, 48); + final byte[] sharedEC = Objects.requireNonNull(ECDH_B163.ecdh_generate_shared(privateEC, remotePublicEC)); + final int encryptedSequenceNumber = BLETypeConversions.toUint32(sharedEC, 0); + + final byte[] secretKey = getSecretKey(); + for (int i = 0; i < 16; i++) { + finalSharedSessionAES[i] = (byte) (sharedEC[i + 8] ^ secretKey[i]); + } + + LOG.debug("Shared Session Key: {}", GB.hexdump(finalSharedSessionAES)); + getSupport().setEncryptionParameters(encryptedSequenceNumber, finalSharedSessionAES); + + try { + final byte[] encryptedRandom1 = CryptoUtils.encryptAES(remoteRandom, secretKey); + final 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); + write("send double encrypted random to device", command); + } + } catch (final Exception e) { + LOG.error("AES encryption failed", e); + } + return; + case CMD_SESSION_KEY: + if (payload[2] == 0x25) { + // wrong auth key + onAuthFailed(); + return; + } else if (payload[2] != HuamiService.SUCCESS) { + LOG.error("Got session key failure, status={}", String.format("0x%02x", payload[0])); + return; + } + + LOG.debug("Auth Success"); + + try { + // Authenticated, now initialize phase 2 + final TransactionBuilder builder = getSupport().createTransactionBuilder("Authenticated, now initialize phase 2"); + builder.add(new SetDeviceStateAction(getSupport().getDevice(), GBDevice.State.INITIALIZING, getContext())); + builder.setCallback(null); // remove init operation as the callback + getSupport().phase2Initialize(builder); + getSupport().phase3Initialize(builder); + getSupport().setInitialized(builder); + getSupport().performImmediately(builder); + } catch (Exception e) { + LOG.error("failed initializing device", e); + } + return; + } + + LOG.warn("Got unknown auth byte {}", String.format("0x%02x", payload[0])); + } + + public void startAuthentication(final TransactionBuilder builder) { + new Random().nextBytes(privateEC); + + final byte[] pub = ECDH_B163.ecdh_generate_public(privateEC); + assert pub != null; + System.arraycopy(pub, 0, publicEC, 0, 48); + + final byte[] sendPubKeyCommand = new byte[48 + 4]; + sendPubKeyCommand[0] = 0x04; + sendPubKeyCommand[1] = 0x02; + sendPubKeyCommand[2] = 0x00; + sendPubKeyCommand[3] = 0x02; + System.arraycopy(publicEC, 0, sendPubKeyCommand, 4, 48); + + write(builder, sendPubKeyCommand); + } + + private void onAuthFailed() { + LOG.error("Authentication failed, disconnecting"); + GB.toast(getContext(), R.string.authentication_failed_check_key, Toast.LENGTH_LONG, GB.WARN); + final GBDevice device = getSupport().getDevice(); + if (device != null) { + GBApplication.deviceService(device).disconnect(); + } + } + + private byte[] getSecretKey() { + final byte[] authKeyBytes = new byte[]{0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45}; + + final Prefs devicePrefs = getDevicePrefs(); + + final String authKey = devicePrefs.getString("authkey", null); + if (authKey != null && !authKey.isEmpty()) { + byte[] srcBytes = authKey.trim().getBytes(); + if (authKey.length() == 34 && authKey.startsWith("0x")) { + srcBytes = GB.hexStringToByteArray(authKey.substring(2)); + } else if (authKey.length() == 32) { + // All Zepp OS devices require a hex key + srcBytes = GB.hexStringToByteArray(authKey); + } + System.arraycopy(srcBytes, 0, authKeyBytes, 0, Math.min(srcBytes.length, 16)); + } + + return authKeyBytes; + } +}