1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-03 17:02:13 +01:00

Added support for Nut devices

This commit is contained in:
TaaviE 2020-10-02 02:01:02 +03:00
parent c0c35a0931
commit 01feaabffd
7 changed files with 1466 additions and 0 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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) {
}
}

View File

@ -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];
}
}
}

View File

@ -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());
}
}

View 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>

View File

@ -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));
}
}