From 424d9cd142b7248e46b2b53be7fd526668970926 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Mon, 21 Mar 2016 23:41:37 +0100 Subject: [PATCH] More work on firmware detection, recognition and validation #234 Should be as robust as possible now. --- .gitignore | 2 + .../miband/AbstractMi1FirmwareInfo.java | 41 +++++++++++++++-- .../miband/AbstractMi1SFirmwareInfo.java | 5 +++ .../miband/AbstractMiFirmwareInfo.java | 15 ++++--- .../devices/miband/Mi1SFirmwareInfo.java | 44 +++++++++++++++++-- .../devices/miband/Mi1SFirmwareInfoFW1.java | 5 +++ .../devices/miband/Mi1SFirmwareInfoFW2.java | 5 +++ .../gadgetbridge/util/ArrayUtils.java | 38 ++++++++++++++++ .../service/devices/miband/FirmwareTest.java | 14 ++++-- 9 files changed, 152 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java diff --git a/.gitignore b/.gitignore index 91efc16b..9bcb005c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ proguard/ *.iml MPChartLib + +fw.dirs diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1FirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1FirmwareInfo.java index 3e423641..f14fd50b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1FirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1FirmwareInfo.java @@ -5,18 +5,37 @@ import android.support.annotation.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; + /** * Some helper methods for Mi1 and Mi1A firmware. */ public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo { private static final Logger LOG = LoggerFactory.getLogger(AbstractMi1FirmwareInfo.class); + private static final byte[] SINGLE_FW_HEADER = new byte[] { + 0, + (byte)0x98, + 0, + (byte)0x20, + (byte)0x89, + 4, + 0, + (byte)0x20 + }; + private static final int SINGLE_FW_HEADER_OFFSET = 0; + private static final int MI1_FW_BASE_OFFSET = 1056; protected AbstractMi1FirmwareInfo(@NonNull byte[] wholeFirmwareBytes) { super(wholeFirmwareBytes); } + @Override + public boolean isSingleMiBandFirmware() { + return true; + } + @Override public int getFirmwareOffset() { return 0; @@ -51,11 +70,11 @@ public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo { @Override protected boolean isGenerallySupportedFirmware() { - if (!isSingleMiBandFirmware()) { - LOG.warn("not a single firmware"); - return false; - } try { + if (!isHeaderValid()) { + LOG.info("unrecognized header"); + return false; + } int majorVersion = getFirmwareVersionMajor(); if (majorVersion == getSupportedMajorVersion()) { return true; @@ -70,5 +89,19 @@ public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo { return false; } + protected boolean isHeaderValid() { + // TODO: not sure if this is a correct check! + return ArrayUtils.equals(SINGLE_FW_HEADER, wholeFirmwareBytes, SINGLE_FW_HEADER_OFFSET, SINGLE_FW_HEADER_OFFSET + SINGLE_FW_HEADER.length); + } + + @Override + public void checkValid() throws IllegalArgumentException { + super.checkValid(); + + if (wholeFirmwareBytes.length < SINGLE_FW_HEADER.length) { + throw new IllegalArgumentException("firmware too small: " + wholeFirmwareBytes.length); + } + } + protected abstract int getSupportedMajorVersion(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java index e22bf9db..cbe8148c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMi1SFirmwareInfo.java @@ -15,4 +15,9 @@ public abstract class AbstractMi1SFirmwareInfo extends AbstractMiFirmwareInfo { public boolean isGenerallyCompatibleWith(GBDevice device) { return MiBandConst.MI_1S.equals(device.getHardwareVersion()); } + + @Override + public boolean isSingleMiBandFirmware() { + return false; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMiFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMiFirmwareInfo.java index f1d4d074..b7e79a31 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMiFirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/AbstractMiFirmwareInfo.java @@ -2,11 +2,14 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; import android.support.annotation.NonNull; +import java.lang.reflect.Array; import java.util.Arrays; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; public abstract class AbstractMiFirmwareInfo { + /** * @param wholeFirmwareBytes * @return @@ -20,6 +23,7 @@ public abstract class AbstractMiFirmwareInfo { throw new IllegalArgumentException("Unsupported data (maybe not even a firmware?)."); } if (candidates.length == 1) { + candidates[0].checkValid(); return candidates[0]; } throw new IllegalArgumentException("don't know for which device the firmware is, matches multiple devices"); @@ -58,6 +62,8 @@ public abstract class AbstractMiFirmwareInfo { protected abstract boolean isGenerallySupportedFirmware(); + protected abstract boolean isHeaderValid(); + public abstract boolean isGenerallyCompatibleWith(GBDevice device); public @NonNull byte[] getFirmwareBytes() { @@ -72,12 +78,9 @@ public abstract class AbstractMiFirmwareInfo { throw new IllegalArgumentException("bad firmware version: " + version); } - public boolean isSingleMiBandFirmware() { - // TODO: not sure if this is a correct check! - if ((wholeFirmwareBytes[7] & 255) != 1) { - return true; - } - return false; + public abstract boolean isSingleMiBandFirmware(); + + public void checkValid() throws IllegalArgumentException { } public AbstractMiFirmwareInfo getFirst() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java index 058bfb43..12c2388f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfo.java @@ -5,6 +5,8 @@ import android.support.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; + /** * FW1 is Mi Band firmware * FW2 is heartrate firmware @@ -12,6 +14,14 @@ import org.slf4j.LoggerFactory; public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo { private static final Logger LOG = LoggerFactory.getLogger(Mi1SFirmwareInfo.class); + private static final byte[] DOUBLE_FW_HEADER = new byte[] { + (byte)0x78, + (byte)0x75, + (byte)0x63, + (byte)0x6b + }; + private static final int DOUBLE_FW_HEADER_OFFSET = 0; + private final Mi1SFirmwareInfoFW1 fw1Info; private final Mi1SFirmwareInfoFW2 fw2Info; @@ -21,6 +31,33 @@ public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo { fw2Info = new Mi1SFirmwareInfoFW2(wholeFirmwareBytes); } + protected boolean isHeaderValid() { + // TODO: not sure if this is a correct check! + return ArrayUtils.equals(DOUBLE_FW_HEADER, wholeFirmwareBytes, DOUBLE_FW_HEADER_OFFSET, DOUBLE_FW_HEADER_OFFSET + DOUBLE_FW_HEADER.length); + } + + @Override + public void checkValid() throws IllegalArgumentException { + super.checkValid(); + int firstEndIndex = getFirst().getFirmwareOffset() + getFirst().getFirmwareLength(); + if (getSecond().getFirmwareOffset() < firstEndIndex) { + throw new IllegalArgumentException("Invalid firmware offsets/lengths: " + getLengthsOffsetsString()); + } + int secondEndIndex = getSecond().getFirmwareOffset(); + if (wholeFirmwareBytes.length < firstEndIndex || wholeFirmwareBytes.length < secondEndIndex) { + throw new IllegalArgumentException("Invalid firmware size, or invalid offsets/lengths: " + getLengthsOffsetsString()); + } + if (getSecond().getFirmwareOffset() < firstEndIndex) { + throw new IllegalArgumentException("Invalid firmware, second fw starts before first fw ends: " + firstEndIndex + "," + getSecond().getFirmwareOffset()); + } + } + + protected String getLengthsOffsetsString() { + return getFirst().getFirmwareOffset() + "," + getFirst().getFirmwareLength() + + "; " + + getSecond().getFirmwareOffset() + "," + getSecond().getFirmwareLength(); + } + @Override public AbstractMiFirmwareInfo getFirst() { return fw1Info; @@ -44,10 +81,11 @@ public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo { @Override protected boolean isGenerallySupportedFirmware() { - if (isSingleMiBandFirmware()) { - return false; - } try { + if (!isHeaderValid()) { + LOG.info("unrecognized header"); + return false; + } return fw1Info.isGenerallySupportedFirmware() && fw2Info.isGenerallySupportedFirmware() && fw1Info.getFirmwareBytes().length > 0 diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW1.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW1.java index 9bb5e572..ff0ad86c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW1.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW1.java @@ -17,6 +17,11 @@ public class Mi1SFirmwareInfoFW1 extends AbstractMi1SFirmwareInfo { super(wholeFirmwareBytes); } + @Override + protected boolean isHeaderValid() { + return true; + } + @Override public int getFirmwareOffset() { return (wholeFirmwareBytes[12] & 255) << 24 diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW2.java index 38484f7b..febec98e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/Mi1SFirmwareInfoFW2.java @@ -16,6 +16,11 @@ public class Mi1SFirmwareInfoFW2 extends AbstractMi1SFirmwareInfo { super(wholeFirmwareBytes); } + @Override + protected boolean isHeaderValid() { + return true; + } + @Override public int getFirmwareOffset() { return (wholeFirmwareBytes[26] & 255) << 24 diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java new file mode 100644 index 00000000..da0aa7ee --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java @@ -0,0 +1,38 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +public class ArrayUtils { + /** + * Checks the two given arrays for equality, but comparing only a subset of the second + * array with the whole first array. + * @param first the whole array to compare against + * @param second the array, of which a subset shall be compared against the whole first array + * @param secondStartIndex the start index (inclusive) of the second array from which to start the comparison + * @param secondEndIndex the end index (exclusive) of the second array until which to compare + * @return whether the first byte array is equal to the specified subset of the second byte array + * @throws IllegalArgumentException when one of the arrays is null or start and end index are wrong + */ + public static boolean equals(byte[] first, byte[] second, int secondStartIndex, int secondEndIndex) { + if (first == null) { + throw new IllegalArgumentException("first must not be null"); + } + if (second == null) { + throw new IllegalArgumentException("second must not be null"); + } + if (secondStartIndex >= secondEndIndex) { + throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex"); + } + if (second.length < secondEndIndex) { + throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex"); + } + if (first.length < secondEndIndex) { + return false; + } + int len = secondEndIndex - secondStartIndex; + for (int i = 0; i < len; i++) { + if (first[i] != second[secondStartIndex + i]) { + return false; + } + } + return true; + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/FirmwareTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/FirmwareTest.java index 8ceee215..2d81aa37 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/FirmwareTest.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/FirmwareTest.java @@ -1,6 +1,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; @@ -12,7 +13,7 @@ import java.util.Arrays; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper; import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; -@Ignore("Disabled for travis -- needs vm parameter -DMiFirmwareDir=/path/to/firmware/directory/") +//@Ignore("Disabled for travis -- needs vm parameter -DMiFirmwareDir=/path/to/firmware/directory/") public class FirmwareTest { private static final long MAX_FILE_SIZE_BYTES = 1024 * 1024; // 1MB @@ -24,6 +25,11 @@ public class FirmwareTest { private static final int SINGLE = 1; private static final int DOUBLE = 2; + @BeforeClass + public static void setupSuite() { + getFirmwareDir(); // throws if firmware directory not available + } + @Test public void testFirmwareMi1() throws Exception { byte[] wholeFw = getFirmwareMi(); @@ -124,11 +130,11 @@ public class FirmwareTest { return info; } - private File getFirmwareDir() { + private static File getFirmwareDir() { String path = System.getProperty("MiFirmwareDir"); - Assert.assertNotNull(path); + Assert.assertNotNull("You must run this test with -DMiFirmwareDir=/path/to/directory/with/miband/firmwarefiles/", path); File dir = new File(path); - Assert.assertTrue(dir.isDirectory()); + Assert.assertTrue("System property MiFirmwareDir should point to a directory continaing the Mi Band firmware files", dir.isDirectory()); return dir; }