mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-28 04:46:51 +01:00
Added support for Nut devices
This commit is contained in:
parent
c0c35a0931
commit
01feaabffd
@ -43,6 +43,7 @@ vendor's servers.
|
||||
* HPlus Devices (e.g. ZeBand) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/HPlus)
|
||||
* iTag [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/iTag)
|
||||
* ID115
|
||||
* Nut Mini 3, Nut 2 and possibly others [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Nut)
|
||||
* JYou Y5
|
||||
* Lefun
|
||||
* Lenovo Watch 9
|
||||
|
@ -0,0 +1,133 @@
|
||||
/* Copyright (C) 2020 Taavi Eomäe
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.nut;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||
|
||||
public class NutConstants {
|
||||
/**
|
||||
* Just battery info
|
||||
*/
|
||||
public static final UUID SERVICE_BATTERY = GattService.UUID_SERVICE_BATTERY_SERVICE;
|
||||
public static final UUID CHARAC_BATTERY_INFO = UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
/**
|
||||
* Device info available.
|
||||
**/
|
||||
public static final UUID SERVICE_DEVICE_INFO = GattService.UUID_SERVICE_DEVICE_INFORMATION;
|
||||
/**
|
||||
* Firmware version.
|
||||
* Used with {@link NutConstants#SERVICE_DEVICE_INFO}
|
||||
*/
|
||||
public static final UUID CHARAC_FIRMWARE_VERSION = UUID.fromString("00002a26-0000-1000-8000-00805f9b34fb");
|
||||
/**
|
||||
* System ID.
|
||||
* Used with {@link NutConstants#SERVICE_DEVICE_INFO}
|
||||
*/
|
||||
public static final UUID CHARAC_SYSTEM_ID = UUID.fromString("00002a23-0000-1000-8000-00805f9b34fb");
|
||||
/**
|
||||
* Hardware version.
|
||||
* Used with {@link NutConstants#SERVICE_DEVICE_INFO}
|
||||
*/
|
||||
public static final UUID CHARAC_HARDWARE_VERSION = UUID.fromString("00002a27-0000-1000-8000-00805f9b34fb");
|
||||
/**
|
||||
* Manufacturer name.
|
||||
* Used with {@link NutConstants#SERVICE_DEVICE_INFO}
|
||||
*/
|
||||
public static final UUID CHARAC_MANUFACTURER_NAME = UUID.fromString("00002a29-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
|
||||
/**
|
||||
* Link loss alert service.
|
||||
*/
|
||||
public static final UUID SERVICE_LINK_LOSS = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
|
||||
/**
|
||||
* Immediate alert service.
|
||||
*/
|
||||
public static final UUID SERVICE_IMMEDIATE_ALERT = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb");
|
||||
/**
|
||||
* Immediate alert level
|
||||
* Used with {@link NutConstants#SERVICE_IMMEDIATE_ALERT}
|
||||
*/
|
||||
public static final UUID CHARAC_LINK_LOSS_ALERT_LEVEL = UUID.fromString("00002a06-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
|
||||
/**
|
||||
* Proprietary command endpoint.
|
||||
* TODO: Anything else in this service on "Focus"?
|
||||
*/
|
||||
public static final UUID SERVICE_PROPRIETARY_NUT = UUID.fromString("0000ff00-0000-1000-8000-00805f9b34fb");
|
||||
/**
|
||||
* Shutdown or reset.
|
||||
* Used with {@link NutConstants#SERVICE_PROPRIETARY_NUT}
|
||||
*/
|
||||
public static final UUID CHARAC_CHANGE_POWER = UUID.fromString("0000ff01-0000-1000-8000-00805f9b34fb");
|
||||
/**
|
||||
* Commands for proprietary service.
|
||||
* Used with {@link NutConstants#SERVICE_PROPRIETARY_NUT}
|
||||
*/
|
||||
public static final UUID CHARAC_DFU_PW = UUID.fromString("0000ff02-0000-1000-8000-00805f9b34fb");
|
||||
/**
|
||||
* Authentication using 16-byte key?
|
||||
* Used with {@link NutConstants#SERVICE_PROPRIETARY_NUT}
|
||||
* TODO: Exists only on Nut Mini?
|
||||
*/
|
||||
public static final UUID CHARAC_AUTH_STATUS = UUID.fromString("0000ff05-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
|
||||
/**
|
||||
* Ringing configuration.
|
||||
* TODO: Exact purpose?
|
||||
*/
|
||||
public static final UUID SERVICE_UNKNOWN_2 = UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb");
|
||||
/**
|
||||
* Ringing configuration.
|
||||
* Used with {@link NutConstants#SERVICE_UNKNOWN_2}
|
||||
* TODO: Something else on other devices?
|
||||
*/
|
||||
public static final UUID CHARAC_UNKNOWN_2 = UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
|
||||
/**
|
||||
* Very little mention online, specific to Nut devices?
|
||||
*/
|
||||
public static final UUID UNKNOWN_3 = UUID.fromString("00001530-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
/**
|
||||
* Concatenates two byte arrays
|
||||
* In reverse
|
||||
* Pads with zeros when too short
|
||||
* Truncates when too long
|
||||
*/
|
||||
public static byte[] assemblePacket(byte[] arr1, byte[] arr2) {
|
||||
byte[] result = new byte[16];
|
||||
for (int i = 0; i < Math.min(arr2.length, 8); i++) {
|
||||
// Reverse the array - 0-indexed - start shorter arrays from "0", truncate longer ones - current index
|
||||
// = source array, start from offset on bigger arrays
|
||||
result[8 - 1 - (8 - Math.min(arr2.length, 8)) - i] = arr2[i + Math.max((arr2.length - 8), 0)];
|
||||
}
|
||||
for (int i = 0; i < Math.min(arr1.length, 8); i++) {
|
||||
result[16 - 1 - (8 - Math.min(arr1.length, 8)) - i] = arr1[i + Math.max((arr1.length - 8), 0)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Daniele Gobbetti, Taavi Eomäe
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.nut;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class NutCoordinator extends AbstractDeviceCoordinator {
|
||||
@Override
|
||||
@NonNull
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
String name = candidate.getDevice().getName();
|
||||
if (name != null && name.toLowerCase().startsWith("nut")) {
|
||||
return DeviceType.NUTMINI;
|
||||
}
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_ASK;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public Collection<? extends ScanFilter> createBLEScanFilters() {
|
||||
ScanFilter filter = new ScanFilter.Builder()
|
||||
.setDeviceName("nut") // Nut Mini
|
||||
.build();
|
||||
return Collections.singletonList(filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.NUTMINI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_nutmini,
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScreenshots() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Nut";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCalendarEvents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
return false; //TODO: RRSI
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
|
||||
}
|
||||
}
|
@ -0,0 +1,266 @@
|
||||
/* Copyright (C) 2020 Taavi Eomäe
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.nut;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class NutKey {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NutKey.class);
|
||||
|
||||
/**
|
||||
* Different from {@link GB#hexStringToByteArray} because
|
||||
* it returns an array of zero bytes when it's given zero bytes
|
||||
* <p>
|
||||
* https://stackoverflow.com/a/140430/4636860
|
||||
*
|
||||
* @param encoded hexadecimal string like "0xcafebabe", "DEADBEEF" or "feeddead"
|
||||
* @return resulting byte array
|
||||
*/
|
||||
public static byte[] hexStringToByteArrayNut(String encoded) {
|
||||
if (encoded.startsWith("0x")) {
|
||||
encoded = encoded.substring(2);
|
||||
}
|
||||
|
||||
if ((encoded.length() % 2) != 0) {
|
||||
throw new IllegalArgumentException("Input string must contain an even number of characters");
|
||||
}
|
||||
|
||||
final byte[] result = new byte[encoded.length() / 2];
|
||||
final char[] enc = encoded.toCharArray();
|
||||
for (int i = 0; i < enc.length; i += 2) {
|
||||
result[i / 2] = (byte) Integer.parseInt(String.valueOf(enc[i]) + enc[i + 1], 16);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array as hexadecimal string space delimited
|
||||
*
|
||||
* @param bytes bytes to return
|
||||
* @return returns
|
||||
*/
|
||||
public static String bytesToHex2(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 3];
|
||||
for (int j = 0; j < bytes.length; j++) {
|
||||
hexChars[j * 3] = GB.HEX_CHARS[(bytes[j] & 0xFF) >>> 4];
|
||||
hexChars[j * 3 + 1] = GB.HEX_CHARS[(bytes[j] & 0xFF) & 0x0F];
|
||||
hexChars[j * 3 + 2] = " ".toCharArray()[0];
|
||||
}
|
||||
return new String(hexChars).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an assembled packet based on inputs
|
||||
*
|
||||
* @param mac the mac of the target device (AA:BB:CC:DD:EE:FF)
|
||||
* @param challenge the received challenge, THE ENTIRE PAYLOAD WITH PREAMBLE!
|
||||
* @param key1 first key
|
||||
* @param key2 second key
|
||||
* @return assembled packet (without the preamble!)
|
||||
*/
|
||||
public static byte[] passwordGeneration(String mac, byte[] challenge, BigInteger key1, BigInteger key2) {
|
||||
if (challenge[0] != 0x01) {
|
||||
throw new IllegalArgumentException("Challenge must be given with the preamble");
|
||||
}
|
||||
|
||||
byte[] mac_as_bytes = macAsByteArray(mac);
|
||||
byte[] correct_challenge = new byte[challenge.length - 1];
|
||||
System.arraycopy(challenge, 1, correct_challenge, 0, challenge.length - 1);
|
||||
ArrayUtils.reverse(correct_challenge);
|
||||
BigInteger c = new BigInteger(1, mac_as_bytes).add(new BigInteger(1, correct_challenge));
|
||||
|
||||
BigInteger max64 = BigInteger.ONE.add(BigInteger.ONE).pow(64).subtract(BigInteger.ONE);
|
||||
BigInteger tmp1 = key2.xor(max64);
|
||||
BigInteger result1;
|
||||
if (c.compareTo(tmp1) > 0) {
|
||||
result1 = c.add(key1).subtract(tmp1);
|
||||
} else {
|
||||
result1 = key1;
|
||||
}
|
||||
|
||||
BigInteger result2;
|
||||
if (key2.remainder(BigInteger.ONE.add(BigInteger.ONE)).compareTo(BigInteger.ONE) == 0) {
|
||||
result2 = key2.add(c);
|
||||
} else {
|
||||
result2 = key2.multiply(BigInteger.ONE.add(BigInteger.ONE)).add(c);
|
||||
}
|
||||
|
||||
return byteArraysConcatReverseWithPad(result1.toByteArray(), result2.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the password generation into keys
|
||||
* <p>
|
||||
* This assumes you have:
|
||||
* The MAC of the device, the challenge and response payload
|
||||
* <p>
|
||||
* See also {@link NutKey#passwordGeneration}
|
||||
*
|
||||
* @param challenge the RECEIVED and COMPLETE payload (truncated accordingly)
|
||||
* @param response the SENT and COMPLETE payload
|
||||
* @param deviceMac colon-separated MAC address of the Nut as a string
|
||||
*/
|
||||
public static Map.Entry<BigInteger, BigInteger> reversePasswordGeneration(byte[] challenge,
|
||||
byte[] response,
|
||||
String deviceMac) {
|
||||
// The two arrays that were concat. with byteArraysConcatReverseWithPad(orig1, orig2)
|
||||
byte[] original1 = new byte[8];
|
||||
byte[] original2 = new byte[8];
|
||||
|
||||
// The response without preamble
|
||||
if (response[0] != 0x02) {
|
||||
throw new IllegalArgumentException("Response always begins with 0x02");
|
||||
}
|
||||
byte[] cleanResponse = new byte[16];
|
||||
System.arraycopy(response, 1, cleanResponse, 0, cleanResponse.length);
|
||||
|
||||
// The challenge without preamble
|
||||
byte[] cleanChall = new byte[4];
|
||||
System.arraycopy(challenge, 1, cleanChall, 0, cleanChall.length);
|
||||
|
||||
// Reverse the two arrays sent as a response
|
||||
byteArraysDeConcatReverseWithPad(cleanResponse, original1, original2);
|
||||
|
||||
// Two common components in the equation
|
||||
BigInteger a = new BigInteger(1, macAsByteArray(deviceMac));
|
||||
byte[] cleanChallTmp = cleanChall.clone();
|
||||
ArrayUtils.reverse(cleanChallTmp);
|
||||
BigInteger b = new BigInteger(1, cleanChallTmp);
|
||||
BigInteger c = a.add(b);
|
||||
BigInteger max64 = BigInteger.ONE.add(BigInteger.ONE).pow(64).subtract(BigInteger.ONE);
|
||||
|
||||
// We don't know actual keys used yet
|
||||
// There's two possibilities,
|
||||
// either it's directly what's in the packet
|
||||
// orig1 = key1
|
||||
BigInteger key1a = new BigInteger(1, original1);
|
||||
// Or it's derived from key2 using this formula :S
|
||||
// orig1 = c + key1 - (key2 XOR (2^64 - 1)
|
||||
// see below when it might be needed
|
||||
|
||||
// It's either just
|
||||
// orig2 = key2 + c
|
||||
BigInteger key2a = new BigInteger(1, original2).subtract(c);
|
||||
// alternatively
|
||||
// orig2 = 2 * key2 + c
|
||||
BigInteger key2b = new BigInteger(1, original2).multiply(BigInteger.ONE.add(BigInteger.ONE)).subtract(c);
|
||||
|
||||
// Now we have key2a, key2b, key1a,
|
||||
// trying to determine if we can do with just those,
|
||||
// or need to continue
|
||||
byte[] key1a2aresult = passwordGeneration(deviceMac, challenge, key1a, key2a);
|
||||
LOG.debug("Result1a2a:02 %s\n", bytesToHex2(key1a2aresult));
|
||||
|
||||
if (java.util.Arrays.equals(key1a2aresult, cleanResponse)) {
|
||||
LOG.debug("Found key1a & key2a are correct, DONE!");
|
||||
return new AbstractMap.SimpleEntry<>(key1a, key2a);
|
||||
}
|
||||
// Unsuccessful, let's try key1a with key2b
|
||||
|
||||
byte[] key1a2bresult = passwordGeneration(deviceMac, challenge, key1a, key2b);
|
||||
LOG.debug("Result1a2b:02 %s\n", bytesToHex2(key1a2bresult));
|
||||
|
||||
if (java.util.Arrays.equals(key1a2bresult, cleanResponse)) {
|
||||
LOG.debug("Found key1a & key2b are correct, DONE!");
|
||||
return new AbstractMap.SimpleEntry<>(key1a, key2b);
|
||||
}
|
||||
|
||||
// If we're still not done, we have to calculate two possible key1b-s
|
||||
// one for key2a, other for key2b
|
||||
// key1 = c + (key2 XOR (2^64 - 1) + orig1
|
||||
BigInteger key1b2a = c.add(key2a.xor(max64)).add(new BigInteger(1, original1));
|
||||
byte[] key1b2aresult = passwordGeneration(deviceMac, challenge, key1b2a, key2a);
|
||||
LOG.debug("Result1b2a:02 %s\n", bytesToHex2(key1b2aresult));
|
||||
|
||||
if (java.util.Arrays.equals(key1b2aresult, cleanResponse)) {
|
||||
LOG.debug("Found key1b2a & key2b are correct, DONE!");
|
||||
return new AbstractMap.SimpleEntry<>(key1b2a, key2b);
|
||||
}
|
||||
|
||||
BigInteger key1b2b = c.add(key2b.xor(max64)).add(new BigInteger(1, original1));
|
||||
byte[] key1b2bresult = passwordGeneration(deviceMac, challenge, key1b2b, key2b);
|
||||
LOG.debug("Result1b2b:02 %s\n", bytesToHex2(key1b2bresult));
|
||||
|
||||
if (java.util.Arrays.equals(key1b2bresult, cleanResponse)) {
|
||||
LOG.debug("Found key1b2b & key2b are correct, DONE!");
|
||||
return new AbstractMap.SimpleEntry<>(key1b2b, key2b);
|
||||
}
|
||||
|
||||
LOG.warn("Input might be incorrect, a correct key was not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the MAC address into an array of bytes
|
||||
*
|
||||
* @param address MAC address
|
||||
* @return byte[] containing the MAC address bytes
|
||||
*/
|
||||
public static byte[] macAsByteArray(String address) {
|
||||
//noinspection DynamicRegexReplaceableByCompiledPattern
|
||||
return hexStringToByteArrayNut(address.replace(":", ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates two byte arrays in reverse and pads with zeros
|
||||
*
|
||||
* @param arr1 first array to concatenate
|
||||
* @param arr2 second array to concatenate
|
||||
* @return 16 bytes that contain the array in reverse, zeros if any parameter is empty
|
||||
*/
|
||||
public static byte[] byteArraysConcatReverseWithPad(byte[] arr1, byte[] arr2) {
|
||||
byte[] result = new byte[16];
|
||||
for (int i = 0; i < Math.min(arr2.length, 8); i++) {
|
||||
// Reverse the array - 0-indexed - start shorter arrays from "0" - byte index to handle
|
||||
result[8 - 1 - (8 - Math.min(arr2.length, 8)) - i] = arr2[i + Math.max((arr2.length - 8), 0)];
|
||||
}
|
||||
for (int i = 0; i < Math.min(arr1.length, 8); i++) {
|
||||
result[16 - 1 - (8 - Math.min(arr1.length, 8)) - i] = arr1[i + Math.max((arr1.length - 8), 0)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* De-concatenates two byte arrays in reverse,
|
||||
* places them in specified destinations,
|
||||
* <p>
|
||||
* 16 bytes that contain the array in reverse,
|
||||
* zeros if any parameter is empty
|
||||
*
|
||||
* @param input array to de-concatenate, 16 bytes
|
||||
*/
|
||||
public static void byteArraysDeConcatReverseWithPad(byte[] input, byte[] dest1, byte[] dest2) {
|
||||
if (input.length != 16) {
|
||||
throw new IllegalArgumentException("Input must be 16 bytes!");
|
||||
}
|
||||
for (int i = 0; i < 8; i++) {
|
||||
dest2[8 - 1 - i] = input[i];
|
||||
}
|
||||
for (int i = 8; i < 16; i++) {
|
||||
dest1[16 - 1 - i] = input[i];
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,632 @@
|
||||
/* Copyright (C) 2016-2020 Andreas Shimokawa, Carsten Pfeiffer, Taavi Eomäe
|
||||
|
||||
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 <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.nut;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
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.battery.BatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class NutSupport extends AbstractBTLEDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(NutSupport.class);
|
||||
|
||||
private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
|
||||
|
||||
private final DeviceInfoProfile<NutSupport> deviceInfoProfile;
|
||||
private final BatteryInfoProfile<NutSupport> batteryInfoProfile;
|
||||
private final IntentListener listener = new IntentListener() {
|
||||
@Override
|
||||
public void notify(Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) {
|
||||
handleDeviceInfo((DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
|
||||
} else if (action.equals(BatteryInfoProfile.ACTION_BATTERY_INFO)) {
|
||||
handleBatteryInfo((BatteryInfo) intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO));
|
||||
} else {
|
||||
LOG.warn("Unhandled intent given to listener");
|
||||
}
|
||||
}
|
||||
};
|
||||
private SharedPreferences prefs = null;
|
||||
/**
|
||||
* It uses the proprietary Nut interface.
|
||||
*/
|
||||
private boolean proprietary = false;
|
||||
/**
|
||||
* Proprietary Nut interface needs authentication.
|
||||
* <p>
|
||||
* Don't write characteristics until authenticated.
|
||||
* <p>
|
||||
* Will disconnect in a minute if you don't authenticate.
|
||||
*/
|
||||
private boolean authenticated = true;
|
||||
/**
|
||||
* The two keys used for authentication
|
||||
*/
|
||||
private BigInteger key1;
|
||||
private BigInteger key2;
|
||||
|
||||
|
||||
public NutSupport() {
|
||||
super(LOG);
|
||||
addSupportedService(NutConstants.SERVICE_BATTERY);
|
||||
addSupportedService(NutConstants.SERVICE_DEVICE_INFO);
|
||||
addSupportedService(NutConstants.SERVICE_IMMEDIATE_ALERT);
|
||||
addSupportedService(NutConstants.SERVICE_LINK_LOSS);
|
||||
addSupportedService(NutConstants.SERVICE_PROPRIETARY_NUT);
|
||||
addSupportedService(NutConstants.SERVICE_UNKNOWN_2);
|
||||
|
||||
deviceInfoProfile = new DeviceInfoProfile<>(this);
|
||||
deviceInfoProfile.addListener(listener);
|
||||
addSupportedProfile(deviceInfoProfile);
|
||||
|
||||
batteryInfoProfile = new BatteryInfoProfile<>(this);
|
||||
batteryInfoProfile.addListener(listener);
|
||||
addSupportedProfile(batteryInfoProfile);
|
||||
}
|
||||
|
||||
private void handleBatteryInfo(BatteryInfo info) {
|
||||
LOG.info("Received Nut battery info");
|
||||
batteryCmd.level = (short) info.getPercentCharged();
|
||||
handleGBDeviceEvent(batteryCmd);
|
||||
}
|
||||
|
||||
private void handleDeviceInfo(DeviceInfo info) {
|
||||
LOG.info("Received Nut device info");
|
||||
LOG.info(String.valueOf(info));
|
||||
GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo();
|
||||
if (info.getHardwareRevision() != null) {
|
||||
versionInfo.hwVersion = info.getHardwareRevision();
|
||||
}
|
||||
if (info.getFirmwareRevision() != null) {
|
||||
versionInfo.fwVersion = info.getFirmwareRevision();
|
||||
}
|
||||
|
||||
handleGBDeviceEvent(versionInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
|
||||
|
||||
// Init prefs
|
||||
prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
|
||||
loadKeysFromPrefs();
|
||||
|
||||
LOG.debug("Requesting device info!");
|
||||
deviceInfoProfile.requestDeviceInfo(builder);
|
||||
batteryInfoProfile.requestBatteryInfo(builder);
|
||||
|
||||
// If this characteristic exists, it has proprietary Nut interface
|
||||
this.proprietary = (getCharacteristic(NutConstants.CHARAC_AUTH_STATUS) != null);
|
||||
|
||||
if (proprietary) {
|
||||
this.authenticated = false;
|
||||
/**
|
||||
* Part of {@link NutConstants.SERVICE_PROPRIETARY_NUT}
|
||||
* Enables proprietary notification
|
||||
*/
|
||||
builder.notify(getCharacteristic(NutConstants.CHARAC_AUTH_STATUS), true);
|
||||
LOG.info("Enabled authentication status notify");
|
||||
|
||||
/**
|
||||
* Part of {@link NutConstants.SERVICE_UNKNOWN_2}
|
||||
* Enables button-press notify
|
||||
*/
|
||||
builder.notify(getCharacteristic(NutConstants.CHARAC_UNKNOWN_2), true);
|
||||
} else {
|
||||
/**
|
||||
* Part of {@link NutConstants.SERVICE_UNKNOWN_1_WEIRDNESS}
|
||||
* Enables button-press notify
|
||||
*/
|
||||
builder.notify(getCharacteristic(NutConstants.CHARAC_CHANGE_POWER), true);
|
||||
}
|
||||
|
||||
readDeviceInfo();
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteNotification(int id) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCallState(CallSpec callSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicState(MusicStateSpec stateSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicInfo(MusicSpec musicSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeSteps(boolean enable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallApp(Uri uri) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppInfoReq() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppStart(UUID uuid, boolean start) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppDelete(UUID uuid) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppReorder(UUID[] uuids) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(int dataTypes) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReset(int flags) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeartRateTest() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindDevice(boolean enable) {
|
||||
deviceImmediateAlert(enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetConstantVibration(int intensity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScreenshotReq() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableHeartRateSleepSupport(boolean enable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetHeartRateMeasurementInterval(int seconds) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteCalendarEvent(byte type, long id) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic) {
|
||||
if (super.onCharacteristicChanged(gatt, characteristic)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
UUID characteristicUUID = characteristic.getUuid();
|
||||
if (characteristicUUID.equals(NutConstants.CHARAC_AUTH_STATUS)) {
|
||||
handleAuthResult(characteristic.getValue());
|
||||
return true;
|
||||
}
|
||||
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicRead(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic,
|
||||
int status) {
|
||||
if (super.onCharacteristicRead(gatt, characteristic, status)) {
|
||||
return true;
|
||||
}
|
||||
UUID characteristicUUID = characteristic.getUuid();
|
||||
|
||||
if (characteristicUUID.equals(NutConstants.CHARAC_SYSTEM_ID)) {
|
||||
// TODO: Handle System ID read
|
||||
return true;
|
||||
}
|
||||
LOG.info("Unhandled characteristic read: " + characteristicUUID);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadConfiguration(String config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables link loss alert
|
||||
*/
|
||||
private void deviceLinkLossAlert(boolean enable) {
|
||||
UUID charac;
|
||||
if (this.proprietary) {
|
||||
/** Part of {@link NutConstants.SERVICE_PROPRIETARY_NUT} */
|
||||
charac = NutConstants.CHARAC_CHANGE_POWER;
|
||||
} else {
|
||||
/** Part of {@link NutConstants.SERVICE_IMMEDIATE_ALERT} */
|
||||
charac = NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL;
|
||||
}
|
||||
|
||||
byte[] payload = new byte[]{(byte) (enable ? 0x00 : 0x01)};
|
||||
if (enable) {
|
||||
writeCharacteristic("Enable link loss alert", charac, payload);
|
||||
} else {
|
||||
writeCharacteristic("Disable link loss alert", charac, payload);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should trigger an immediate alert
|
||||
*
|
||||
* @param enable turn on or not
|
||||
*/
|
||||
private void deviceImmediateAlert(boolean enable) {
|
||||
UUID charac;
|
||||
if (this.proprietary) {
|
||||
/** Part of {@link NutConstants.SERVICE_IMMEDIATE_ALERT} */
|
||||
charac = NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL;
|
||||
if (!authenticated) {
|
||||
LOG.warn("Not authenticated, can't alert");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
/** Part of {@link NutConstants.SERVICE_PROPRIETARY_NUT} */
|
||||
charac = NutConstants.CHARAC_CHANGE_POWER;
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
writeCharacteristic("Start alert", charac, new byte[]{(byte) 0x04});
|
||||
} else {
|
||||
writeCharacteristic("Stop alert", charac, new byte[]{(byte) 0x03});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will write a new key to the device
|
||||
* <p>
|
||||
* However, <b>it is irreversible</b>,
|
||||
* if you can't generate the right packets,
|
||||
* the device is basically bricked!
|
||||
* <p>
|
||||
* If you can generate the correct packets,
|
||||
* it can be reset... somehow
|
||||
*
|
||||
* @param key key
|
||||
*/
|
||||
private void deviceWriteNewKey(byte[] key) {
|
||||
// TODO: Determine each nuance of how this
|
||||
// works before using it!
|
||||
byte[] result_payload = new byte[key.length + 1];
|
||||
result_payload[0] = (byte) 0x04;
|
||||
System.arraycopy(key, 0, result_payload, 1, key.length);
|
||||
|
||||
writeCharacteristic("Write new key",
|
||||
NutConstants.CHARAC_DFU_PW,
|
||||
result_payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the device off
|
||||
*/
|
||||
private void deviceShutdown() {
|
||||
writeCharacteristic("Shutdown", NutConstants.CHARAC_CHANGE_POWER, new byte[]{0x06});
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches the device to Nordic's DFU mode
|
||||
*/
|
||||
private void deviceDFU() {
|
||||
writeCharacteristic("Enable DFU mode", NutConstants.CHARAC_DFU_PW, new byte[]{0x14});
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies how long the alert lasts
|
||||
*
|
||||
* @param duration in seconds (I think)
|
||||
*/
|
||||
private void deviceWriteAlertDuration(int duration) {
|
||||
if (duration == 0) {
|
||||
duration = 15;
|
||||
}
|
||||
|
||||
UUID charac;
|
||||
if (this.proprietary) {
|
||||
charac = NutConstants.CHARAC_DFU_PW;
|
||||
} else {
|
||||
charac = NutConstants.CHARAC_CHANGE_POWER;
|
||||
}
|
||||
|
||||
writeCharacteristic("Write alert duration",
|
||||
charac,
|
||||
new byte[]{37, (byte) duration});
|
||||
}
|
||||
|
||||
private void readHardwareInfo() {
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(NutConstants.CHARAC_HARDWARE_VERSION);
|
||||
if (characteristic != null &&
|
||||
((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0)) {
|
||||
readCharacteristic("Device read hardware",
|
||||
NutConstants.CHARAC_HARDWARE_VERSION);
|
||||
}
|
||||
|
||||
BluetoothGattCharacteristic characteristic1 = getCharacteristic(NutConstants.CHARAC_MANUFACTURER_NAME);
|
||||
if (characteristic1 != null &&
|
||||
(characteristic1.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
|
||||
readCharacteristic("Read manufacturer",
|
||||
NutConstants.CHARAC_MANUFACTURER_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
private void readFirmwareInfo() {
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(NutConstants.CHARAC_FIRMWARE_VERSION);
|
||||
if (characteristic != null &&
|
||||
(characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
|
||||
readCharacteristic("Read firmware version",
|
||||
NutConstants.CHARAC_FIRMWARE_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
private void readBatteryInfo() {
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(NutConstants.CHARAC_BATTERY_INFO);
|
||||
if (characteristic != null &&
|
||||
(characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
|
||||
readCharacteristic("Read battery info",
|
||||
NutConstants.CHARAC_BATTERY_INFO);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the three keys from device-specific shared preferences
|
||||
*/
|
||||
private void loadKeysFromPrefs() {
|
||||
if (prefs != null) {
|
||||
LOG.info("Reading keys");
|
||||
key1 = new BigInteger(prefs.getString("nut_packet_key_1", "0"));
|
||||
key2 = new BigInteger(prefs.getString("nut_packet_key_2", "0"));
|
||||
if (key1.equals(BigInteger.ZERO) || key2.equals(BigInteger.ZERO)) {
|
||||
byte[] challenge = NutKey.hexStringToByteArrayNut(prefs.getString("nut_packet_challenge", "00"));
|
||||
byte[] response = NutKey.hexStringToByteArrayNut(prefs.getString("nut_response_challenge", "00"));
|
||||
if (Arrays.equals(challenge, new byte[]{0x00}) ||
|
||||
Arrays.equals(response, new byte[]{0x00})) {
|
||||
GB.toast("No key available for the device", Toast.LENGTH_LONG, GB.ERROR);
|
||||
return;
|
||||
}
|
||||
Map.Entry<BigInteger, BigInteger> key = NutKey.reversePasswordGeneration(
|
||||
challenge,
|
||||
response,
|
||||
gbDevice.getAddress()
|
||||
);
|
||||
if (key == null) {
|
||||
GB.toast("No correct key available for the device", Toast.LENGTH_LONG, GB.ERROR);
|
||||
return;
|
||||
}
|
||||
key1 = key.getKey();
|
||||
key2 = key.getValue();
|
||||
LOG.debug("Key was extracted from challenge-response packets");
|
||||
} else {
|
||||
LOG.debug("Key was preset");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the authentication flow of the proprietary Nut protocol
|
||||
* See more: {@link NutConstants#SERVICE_PROPRIETARY_NUT}
|
||||
*
|
||||
* @param received the notify characteristic's content
|
||||
*/
|
||||
public final void handleAuthResult(byte[] received) {
|
||||
if (received != null && received.length != 0) {
|
||||
if (received[0] == 0x01) {
|
||||
// Password is needed
|
||||
// Preamble, counter, rotating key, static key
|
||||
byte[] payload = new byte[1 + 1 + 3 + 12];
|
||||
|
||||
// This is a response to the challenge
|
||||
payload[0] = 0x02;
|
||||
|
||||
// Modify the challenge
|
||||
byte[] response = NutKey.passwordGeneration(gbDevice.getAddress(), received, key1, key2);
|
||||
System.arraycopy(response, 0, payload, 1, response.length);
|
||||
|
||||
writeCharacteristic("Authentication",
|
||||
NutConstants.CHARAC_DFU_PW,
|
||||
payload
|
||||
);
|
||||
LOG.debug("Successfully sent auth");
|
||||
} else if (received[0] == 0x03) {
|
||||
if (received[1] == 0x55) {
|
||||
LOG.debug("Successful password attempt or uninitialized");
|
||||
authenticated = true;
|
||||
initChara();
|
||||
} else {
|
||||
LOG.debug("Error authenticating");
|
||||
// TODO: Disconnect
|
||||
}
|
||||
} else if (received[0] == 0x05) {
|
||||
LOG.debug("Password has been set");
|
||||
} else {
|
||||
LOG.debug("Invalid packet");
|
||||
// TODO: Disconnect
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes required characteristics
|
||||
*/
|
||||
private void initChara() {
|
||||
if (proprietary) {
|
||||
writeCharacteristic("Init alert 1", NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL, new byte[]{(byte) 0x00});
|
||||
writeCharacteristic("Init alert 2", NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL, new byte[]{(byte) 0x00});
|
||||
writeCharacteristic("Init alert 3", NutConstants.CHARAC_LINK_LOSS_ALERT_LEVEL, new byte[]{(byte) 0x00});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a read of all the device information characteristics
|
||||
*/
|
||||
private void readDeviceInfo() {
|
||||
readBatteryInfo();
|
||||
readHardwareInfo();
|
||||
readFirmwareInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Just wraps writing into a neat little function
|
||||
*
|
||||
* @param taskName something that describes the task a bit
|
||||
* @param charac the characteristic to write
|
||||
* @param data the data to write
|
||||
*/
|
||||
private void writeCharacteristic(String taskName, UUID charac, byte[] data) {
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(charac);
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder(taskName);
|
||||
builder.write(characteristic, data);
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Just wraps reading into a neat little function
|
||||
*
|
||||
* @param taskName something that describes the task a bit
|
||||
* @param charac the characteristic to read
|
||||
*/
|
||||
private void readCharacteristic(String taskName, UUID charac) {
|
||||
BluetoothGattCharacteristic characteristic = getCharacteristic(charac);
|
||||
|
||||
TransactionBuilder builder = new TransactionBuilder(taskName);
|
||||
builder.read(characteristic);
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
}
|
23
app/src/main/res/xml/devicesettings_nutmini.xml
Normal file
23
app/src/main/res/xml/devicesettings_nutmini.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<EditTextPreference
|
||||
android:dialogTitle="Packet key"
|
||||
android:icon="@drawable/ic_lock_open"
|
||||
android:key="nut_packet_challenge"
|
||||
android:title="Challenge" />
|
||||
<EditTextPreference
|
||||
android:dialogTitle="Challenge key"
|
||||
android:icon="@drawable/ic_lock_open"
|
||||
android:key="nut_packet_response"
|
||||
android:title="Response" />
|
||||
<EditTextPreference
|
||||
android:dialogTitle="Challenge key"
|
||||
android:icon="@drawable/ic_lock_open"
|
||||
android:key="nut_packet_key_1"
|
||||
android:title="Known key 1 (overrides)" />
|
||||
<EditTextPreference
|
||||
android:dialogTitle="Challenge key"
|
||||
android:icon="@drawable/ic_lock_open"
|
||||
android:key="nut_packet_key_2"
|
||||
android:title="Known key 2 (overrides)" />
|
||||
</androidx.preference.PreferenceScreen>
|
@ -0,0 +1,247 @@
|
||||
/* Copyright (C) 2020 Taavi Eomäe
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.nut;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
import static junit.framework.TestCase.assertNotNull;
|
||||
import static junit.framework.TestCase.fail;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.byteArraysConcatReverseWithPad;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.byteArraysDeConcatReverseWithPad;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.bytesToHex2;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.hexStringToByteArrayNut;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.macAsByteArray;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.passwordGeneration;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.nut.NutKey.reversePasswordGeneration;
|
||||
|
||||
public class NutUtilsTest {
|
||||
@Test
|
||||
public void testPasswordGen() {
|
||||
String result = bytesToHex2(passwordGeneration(
|
||||
"00:00:00:00:00:00",
|
||||
new byte[]{1, 0, 0, 0, 0, 0, 0},
|
||||
BigInteger.ZERO,
|
||||
BigInteger.ZERO)
|
||||
);
|
||||
String expected = bytesToHex2(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordGen2() {
|
||||
String result = bytesToHex2(passwordGeneration(
|
||||
"00:00:00:00:00:00",
|
||||
new byte[]{1, 0, 0, 0, 0, 0, 0},
|
||||
BigInteger.ZERO,
|
||||
BigInteger.ZERO)
|
||||
);
|
||||
String expected = bytesToHex2(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordGen3() {
|
||||
String result = bytesToHex2(passwordGeneration(
|
||||
"00:00:00:00:00:00",
|
||||
new byte[]{1, 1, 0, 0, 0, 0, 0},
|
||||
BigInteger.ZERO,
|
||||
BigInteger.ZERO)
|
||||
);
|
||||
String expected = bytesToHex2(new byte[]{0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordGen4() {
|
||||
String result = bytesToHex2(passwordGeneration(
|
||||
"00:00:00:00:00:00",
|
||||
new byte[]{1, 0, 0, 0, 0, 0, 0},
|
||||
new BigInteger(1, new byte[]{1, 0, 0, 0, 0, 0}),
|
||||
BigInteger.ZERO)
|
||||
);
|
||||
String expected = bytesToHex2(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00});
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordGen5() {
|
||||
String result = bytesToHex2(passwordGeneration(
|
||||
"00:00:00:00:00:00",
|
||||
new byte[]{1, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
BigInteger.ZERO,
|
||||
new BigInteger(1, new byte[]{1, 1, 1, 1, 1, 1, 0, 0}))
|
||||
);
|
||||
String expected = bytesToHex2(new byte[]{0x00, 0x00, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordGen6() {
|
||||
String result = bytesToHex2(passwordGeneration(
|
||||
"01:02:03:04:05:06",
|
||||
new byte[]{1, 1, 2, 3, 4, 5, 6, 7, 8},
|
||||
BigInteger.ZERO,
|
||||
BigInteger.ZERO)
|
||||
);
|
||||
String expected = bytesToHex2(new byte[]{0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordGen7() {
|
||||
String result = bytesToHex2(passwordGeneration(
|
||||
"01:02:03:04:05:06",
|
||||
new byte[]{1, 1, 2, 3, 4, 5, 6, 7, 8},
|
||||
new BigInteger(1, new byte[]{1, 2, 3, 4, 5, 6, 7, 8}),
|
||||
new BigInteger(1, new byte[]{1, 2, 3, 4, 5, 6, 7, 8}))
|
||||
);
|
||||
String expected = bytesToHex2(new byte[]{0x17, 0x15, 0x13, 0x11, 0x0f, 0x0d, 0x0b, 0x0a, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01});
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReversePassword() {
|
||||
String challenge = "01e8f0340d000000000000000000000000";
|
||||
String response = "029bbd0fa25aed0000dcfd0c0000000000";
|
||||
String device_mac = "ED:5A:94:CB:98:E4";
|
||||
|
||||
Map.Entry<BigInteger, BigInteger> key = reversePasswordGeneration(
|
||||
hexStringToByteArrayNut(challenge),
|
||||
hexStringToByteArrayNut(response),
|
||||
device_mac
|
||||
);
|
||||
|
||||
assertNotNull(key);
|
||||
assertEquals(new AbstractMap.SimpleEntry<>(new BigInteger("851420"), new BigInteger("996303")), key);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPassword() {
|
||||
String challenge = "011a9b826c000000000000000000000000";
|
||||
String response = "02cd675d015bed0000dcfd0c0000000000";
|
||||
String device_mac = "ED:5A:94:CB:98:E4";
|
||||
|
||||
Map.Entry<BigInteger, BigInteger> key = new AbstractMap.SimpleEntry<>(new BigInteger("851420"), new BigInteger("996303"));
|
||||
|
||||
byte[] result = new byte[17];
|
||||
result[0] = 0x02;
|
||||
System.arraycopy(passwordGeneration(device_mac, hexStringToByteArrayNut(challenge), key.getKey(), key.getValue()), 0, result, 1, 16);
|
||||
|
||||
assertEquals(bytesToHex2(hexStringToByteArrayNut(response)), bytesToHex2(result));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidResponse() {
|
||||
String challenge = "00";
|
||||
String response = "00";
|
||||
String device_mac = "0:00:00:00:00:00";
|
||||
try {
|
||||
reversePasswordGeneration(
|
||||
hexStringToByteArrayNut(challenge),
|
||||
hexStringToByteArrayNut(response),
|
||||
device_mac
|
||||
);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// This is intended behaviour
|
||||
assertNotNull(e);
|
||||
return;
|
||||
}
|
||||
fail();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHexToByteArray() {
|
||||
byte[] arr = hexStringToByteArrayNut("0x0000000000");
|
||||
assertEquals(bytesToHex2(new byte[]{0x00, 0x00, 0x00, 0x00, 0x00}), bytesToHex2(arr));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHexToByteArray2() {
|
||||
byte[] arr = hexStringToByteArrayNut("cafebabe");
|
||||
assertEquals(bytesToHex2(new byte[]{(byte) 0xca, (byte) 0xfe, (byte) 0xba, (byte) 0xbe}), bytesToHex2(arr));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMACToByteArray() {
|
||||
byte[] arr = macAsByteArray("AA:BB:CC:DD:EE:FF");
|
||||
assertEquals(bytesToHex2(
|
||||
new byte[]{
|
||||
(byte) 0xaa,
|
||||
(byte) 0xbb,
|
||||
(byte) 0xcc,
|
||||
(byte) 0xdd,
|
||||
(byte) 0xee,
|
||||
(byte) 0xff
|
||||
}
|
||||
), bytesToHex2(arr));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConcat() {
|
||||
byte[] src1 = new byte[]{1, 2, 3, 4, 5, 6, 7, 8};
|
||||
byte[] src2 = new byte[]{9, 10, 11, 12, 13, 14, 15, 16};
|
||||
assertEquals(
|
||||
bytesToHex2(new byte[]{0x10, 0x0f, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01}),
|
||||
bytesToHex2(byteArraysConcatReverseWithPad(
|
||||
src1,
|
||||
src2)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConcatDeConcat() {
|
||||
byte[] src1 = new byte[]{0, 0, 3, 4, 5, 6, 0, 0};
|
||||
byte[] src2 = new byte[]{0, 0, 7, 8, 9, 1, 0, 0};
|
||||
byte[] dst1 = new byte[8];
|
||||
byte[] dst2 = new byte[8];
|
||||
byteArraysDeConcatReverseWithPad(
|
||||
byteArraysConcatReverseWithPad(
|
||||
src1,
|
||||
src2
|
||||
),
|
||||
dst1,
|
||||
dst2
|
||||
);
|
||||
assertEquals(bytesToHex2(src1), bytesToHex2(dst1));
|
||||
assertEquals(bytesToHex2(src2), bytesToHex2(dst2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConcatDeConcat2() {
|
||||
byte[] src1 = new byte[]{1, 2, 3, 4, 5, 6, 7, 8};
|
||||
byte[] src2 = new byte[]{9, 10, 11, 12, 13, 14, 15, 16};
|
||||
byte[] dst1 = new byte[8];
|
||||
byte[] dst2 = new byte[8];
|
||||
byteArraysDeConcatReverseWithPad(
|
||||
byteArraysConcatReverseWithPad(
|
||||
src1,
|
||||
src2
|
||||
),
|
||||
dst1,
|
||||
dst2
|
||||
);
|
||||
assertEquals(bytesToHex2(src1), bytesToHex2(dst1));
|
||||
assertEquals(bytesToHex2(src2), bytesToHex2(dst2));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user