More work on firmware detection, recognition and validation #234

Should be as robust as possible now.
here
cpfeiffer 2016-03-21 23:41:37 +01:00
parent 1933e2bf10
commit 424d9cd142
9 changed files with 152 additions and 17 deletions

2
.gitignore vendored
View File

@ -29,3 +29,5 @@ proguard/
*.iml *.iml
MPChartLib MPChartLib
fw.dirs

View File

@ -5,18 +5,37 @@ import android.support.annotation.NonNull;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
/** /**
* Some helper methods for Mi1 and Mi1A firmware. * Some helper methods for Mi1 and Mi1A firmware.
*/ */
public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo { public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
private static final Logger LOG = LoggerFactory.getLogger(AbstractMi1FirmwareInfo.class); 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; private static final int MI1_FW_BASE_OFFSET = 1056;
protected AbstractMi1FirmwareInfo(@NonNull byte[] wholeFirmwareBytes) { protected AbstractMi1FirmwareInfo(@NonNull byte[] wholeFirmwareBytes) {
super(wholeFirmwareBytes); super(wholeFirmwareBytes);
} }
@Override
public boolean isSingleMiBandFirmware() {
return true;
}
@Override @Override
public int getFirmwareOffset() { public int getFirmwareOffset() {
return 0; return 0;
@ -51,11 +70,11 @@ public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
@Override @Override
protected boolean isGenerallySupportedFirmware() { protected boolean isGenerallySupportedFirmware() {
if (!isSingleMiBandFirmware()) {
LOG.warn("not a single firmware");
return false;
}
try { try {
if (!isHeaderValid()) {
LOG.info("unrecognized header");
return false;
}
int majorVersion = getFirmwareVersionMajor(); int majorVersion = getFirmwareVersionMajor();
if (majorVersion == getSupportedMajorVersion()) { if (majorVersion == getSupportedMajorVersion()) {
return true; return true;
@ -70,5 +89,19 @@ public abstract class AbstractMi1FirmwareInfo extends AbstractMiFirmwareInfo {
return false; 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(); protected abstract int getSupportedMajorVersion();
} }

View File

@ -15,4 +15,9 @@ public abstract class AbstractMi1SFirmwareInfo extends AbstractMiFirmwareInfo {
public boolean isGenerallyCompatibleWith(GBDevice device) { public boolean isGenerallyCompatibleWith(GBDevice device) {
return MiBandConst.MI_1S.equals(device.getHardwareVersion()); return MiBandConst.MI_1S.equals(device.getHardwareVersion());
} }
@Override
public boolean isSingleMiBandFirmware() {
return false;
}
} }

View File

@ -2,11 +2,14 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import java.lang.reflect.Array;
import java.util.Arrays; import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
public abstract class AbstractMiFirmwareInfo { public abstract class AbstractMiFirmwareInfo {
/** /**
* @param wholeFirmwareBytes * @param wholeFirmwareBytes
* @return * @return
@ -20,6 +23,7 @@ public abstract class AbstractMiFirmwareInfo {
throw new IllegalArgumentException("Unsupported data (maybe not even a firmware?)."); throw new IllegalArgumentException("Unsupported data (maybe not even a firmware?).");
} }
if (candidates.length == 1) { if (candidates.length == 1) {
candidates[0].checkValid();
return candidates[0]; return candidates[0];
} }
throw new IllegalArgumentException("don't know for which device the firmware is, matches multiple devices"); 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 isGenerallySupportedFirmware();
protected abstract boolean isHeaderValid();
public abstract boolean isGenerallyCompatibleWith(GBDevice device); public abstract boolean isGenerallyCompatibleWith(GBDevice device);
public @NonNull byte[] getFirmwareBytes() { public @NonNull byte[] getFirmwareBytes() {
@ -72,12 +78,9 @@ public abstract class AbstractMiFirmwareInfo {
throw new IllegalArgumentException("bad firmware version: " + version); throw new IllegalArgumentException("bad firmware version: " + version);
} }
public boolean isSingleMiBandFirmware() { public abstract boolean isSingleMiBandFirmware();
// TODO: not sure if this is a correct check!
if ((wholeFirmwareBytes[7] & 255) != 1) { public void checkValid() throws IllegalArgumentException {
return true;
}
return false;
} }
public AbstractMiFirmwareInfo getFirst() { public AbstractMiFirmwareInfo getFirst() {

View File

@ -5,6 +5,8 @@ import android.support.annotation.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
/** /**
* FW1 is Mi Band firmware * FW1 is Mi Band firmware
* FW2 is heartrate firmware * FW2 is heartrate firmware
@ -12,6 +14,14 @@ import org.slf4j.LoggerFactory;
public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo { public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo {
private static final Logger LOG = LoggerFactory.getLogger(Mi1SFirmwareInfo.class); 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 Mi1SFirmwareInfoFW1 fw1Info;
private final Mi1SFirmwareInfoFW2 fw2Info; private final Mi1SFirmwareInfoFW2 fw2Info;
@ -21,6 +31,33 @@ public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo {
fw2Info = new Mi1SFirmwareInfoFW2(wholeFirmwareBytes); 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 @Override
public AbstractMiFirmwareInfo getFirst() { public AbstractMiFirmwareInfo getFirst() {
return fw1Info; return fw1Info;
@ -44,10 +81,11 @@ public class Mi1SFirmwareInfo extends AbstractMi1SFirmwareInfo {
@Override @Override
protected boolean isGenerallySupportedFirmware() { protected boolean isGenerallySupportedFirmware() {
if (isSingleMiBandFirmware()) {
return false;
}
try { try {
if (!isHeaderValid()) {
LOG.info("unrecognized header");
return false;
}
return fw1Info.isGenerallySupportedFirmware() return fw1Info.isGenerallySupportedFirmware()
&& fw2Info.isGenerallySupportedFirmware() && fw2Info.isGenerallySupportedFirmware()
&& fw1Info.getFirmwareBytes().length > 0 && fw1Info.getFirmwareBytes().length > 0

View File

@ -17,6 +17,11 @@ public class Mi1SFirmwareInfoFW1 extends AbstractMi1SFirmwareInfo {
super(wholeFirmwareBytes); super(wholeFirmwareBytes);
} }
@Override
protected boolean isHeaderValid() {
return true;
}
@Override @Override
public int getFirmwareOffset() { public int getFirmwareOffset() {
return (wholeFirmwareBytes[12] & 255) << 24 return (wholeFirmwareBytes[12] & 255) << 24

View File

@ -16,6 +16,11 @@ public class Mi1SFirmwareInfoFW2 extends AbstractMi1SFirmwareInfo {
super(wholeFirmwareBytes); super(wholeFirmwareBytes);
} }
@Override
protected boolean isHeaderValid() {
return true;
}
@Override @Override
public int getFirmwareOffset() { public int getFirmwareOffset() {
return (wholeFirmwareBytes[26] & 255) << 24 return (wholeFirmwareBytes[26] & 255) << 24

View File

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

View File

@ -1,6 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband; package nodomain.freeyourgadget.gadgetbridge.service.devices.miband;
import org.junit.Assert; import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
@ -12,7 +13,7 @@ import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandFWHelper;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; 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 { public class FirmwareTest {
private static final long MAX_FILE_SIZE_BYTES = 1024 * 1024; // 1MB 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 SINGLE = 1;
private static final int DOUBLE = 2; private static final int DOUBLE = 2;
@BeforeClass
public static void setupSuite() {
getFirmwareDir(); // throws if firmware directory not available
}
@Test @Test
public void testFirmwareMi1() throws Exception { public void testFirmwareMi1() throws Exception {
byte[] wholeFw = getFirmwareMi(); byte[] wholeFw = getFirmwareMi();
@ -124,11 +130,11 @@ public class FirmwareTest {
return info; return info;
} }
private File getFirmwareDir() { private static File getFirmwareDir() {
String path = System.getProperty("MiFirmwareDir"); 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); 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; return dir;
} }