/* Copyright (C) 2022-2024 Andreas Shimokawa 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.soflow; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.SharedPreferences; import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class SoFlowSupport extends AbstractBTLEDeviceSupport { public static final UUID UUID_CHARACTERISICS_NOTIFICATION = UUID.fromString("60000002-0000-1000-8000-00805f9b34fb"); public static final UUID UUID_CHARACTERISICS_WRITE = UUID.fromString("60000003-0000-1000-8000-00805f9b34fb"); public static final byte[] COMMAND_REQUEST_SESSION = new byte[]{0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; public static final byte[] COMMAND_REQUEST_LOCK = new byte[]{0x05, 0x0e, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; public static final byte[] COMMAND_LOCK = new byte[]{0x05, 0x0c, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; public static final byte[] COMMAND_UNLOCK = new byte[]{0x05, 0x01, 0x06, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; public static final byte[] COMMAND_SET_LIGHT = new byte[]{0x05, 0x47, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; private static final Logger LOG = LoggerFactory.getLogger(SoFlowSupport.class); private final DeviceInfoProfile deviceInfoProfile; private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); private byte[] aesKey; private final byte[] session = new byte[]{0, 0, 0, 0}; public SoFlowSupport() { super(LOG); addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE); addSupportedService(UUID.fromString("60000001-0000-1000-8000-00805f9b34fb")); deviceInfoProfile = new DeviceInfoProfile<>(this); IntentListener mListener = intent -> { String s = intent.getAction(); if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(s)) { handleDeviceInfo(intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); } }; deviceInfoProfile.addListener(mListener); addSupportedProfile(deviceInfoProfile); } @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { aesKey = getSecretKey(); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); requestDeviceInfo(builder); builder.notify(getCharacteristic(UUID_CHARACTERISICS_NOTIFICATION), true); writeEncrypted(builder, COMMAND_REQUEST_SESSION); setInitialized(builder); return builder; } private void requestDeviceInfo(TransactionBuilder builder) { LOG.debug("Requesting Device Info!"); deviceInfoProfile.requestDeviceInfo(builder); } private void setInitialized(TransactionBuilder builder) { builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); } private void writeEncrypted(TransactionBuilder builder, byte[] data) { try { LOG.debug("will encrypt " + GB.hexdump(data)); builder.write(getCharacteristic(UUID_CHARACTERISICS_WRITE), CryptoUtils.encryptAES(data, aesKey)); } catch (Exception e) { LOG.error("error while encrypting data"); } } @Override public boolean useAutoConnect() { return false; } private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) { LOG.warn("Device info: " + info); versionCmd.hwVersion = info.getHardwareRevision(); versionCmd.fwVersion = info.getFirmwareRevision(); handleGBDeviceEvent(versionCmd); } protected byte[] getSecretKey() { byte[] authKeyBytes; SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); String authKey = sharedPrefs.getString("authkey", null); if (authKey != null && !authKey.isEmpty()) { authKey = authKey.trim(); if (authKey.length() == 34 && authKey.startsWith("0x")) { authKeyBytes = GB.hexStringToByteArray(authKey.substring(2)); } else if (authKey.length() == 32) { authKeyBytes = GB.hexStringToByteArray(authKey); } else { return null; } return authKeyBytes; } return null; } @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { if (super.onCharacteristicChanged(gatt, characteristic)) { return true; } UUID characteristicUUID = characteristic.getUuid(); if (UUID_CHARACTERISICS_NOTIFICATION.equals(characteristicUUID)) { try { byte[] data = CryptoUtils.decryptAES(characteristic.getValue(), aesKey); if (data[0] == 0x06 && data[1] == 0x01 && data[2] == 0x07) { session[0] = data[3]; session[1] = data[4]; session[2] = data[5]; session[3] = data[6]; LOG.info("Got session"); COMMAND_LOCK[4] = session[0]; COMMAND_LOCK[5] = session[1]; COMMAND_LOCK[6] = session[2]; COMMAND_LOCK[7] = session[3]; COMMAND_REQUEST_LOCK[4] = session[0]; COMMAND_REQUEST_LOCK[5] = session[1]; COMMAND_REQUEST_LOCK[6] = session[2]; COMMAND_REQUEST_LOCK[7] = session[3]; COMMAND_SET_LIGHT[4] = session[0]; COMMAND_SET_LIGHT[5] = session[1]; COMMAND_SET_LIGHT[6] = session[2]; COMMAND_SET_LIGHT[7] = session[3]; COMMAND_UNLOCK[9] = session[0]; COMMAND_UNLOCK[10] = session[1]; COMMAND_UNLOCK[11] = session[2]; COMMAND_UNLOCK[12] = session[3]; } else { GB.toast(GB.hexdump(data),5,1); } } catch (Exception e) { LOG.error("error while decrypting"); } return true; } LOG.info("Unhandled characteristic changed: " + characteristicUUID); return false; } @Override public void onSendConfiguration(String config) { TransactionBuilder builder; try { builder = performInitialized("Sending configuration for option: " + config); if ("lock".equals(config)) { SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); boolean lock = sharedPrefs.getBoolean("lock", false); if (lock) { writeEncrypted(builder, COMMAND_LOCK); } else { writeEncrypted(builder, COMMAND_UNLOCK); } } builder.queue(getQueue()); } catch (IOException e) { GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e); } } @Override public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (super.onCharacteristicRead(gatt, characteristic, status)) { return true; } UUID characteristicUUID = characteristic.getUuid(); LOG.info("Unhandled characteristic read: " + characteristicUUID); return false; } @Override public void onTestNewFunction() { TransactionBuilder builder; try { builder = performInitialized("request unknown"); writeEncrypted(builder,COMMAND_REQUEST_LOCK); builder.queue(getQueue()); } catch (IOException e) { GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e); } } @Override public boolean getImplicitCallbackModify() { return true; } @Override public boolean getSendWriteRequestResponse() { return false; } }