Skip to content

MASTG-KNOW-0029: File Integrity Checks

There are two topics related to file integrity:

  1. Code integrity checks: You can use CRC checks as an additional protection layer for the app bytecode, native libraries, and important data files. This way the app would only run correctly in its unmodified state, even if the code signature is valid.
  2. File storage integrity checks: The integrity of files that the application stores on the SD card or public storage and the integrity of key-value pairs that are stored in SharedPreferences should be protected.

Sample Implementation - Application Source Code

Integrity checks often calculate a checksum or hash over selected files. Commonly protected files include

  • AndroidManifest.xml,
  • class files *.dex,
  • native libraries (*.so).

The following sample implementation from the Android Cracking blog calculates a CRC over classes.dex and compares it to the expected value.

private void crcTest() throws IOException {
 boolean modified = false;
 // required dex crc value stored as a text string.
 // it could be any invisible layout element
 long dexCrc = Long.parseLong(Main.MyContext.getString(R.string.dex_crc));

 ZipFile zf = new ZipFile(Main.MyContext.getPackageCodePath());
 ZipEntry ze = zf.getEntry("classes.dex");

 if ( ze.getCrc() != dexCrc ) {
  // dex has been modified
  modified = true;
 }
 else {
  // dex not tampered with
  modified = false;
 }
}

Sample Implementation - Storage

When providing integrity on the storage itself, you can either create an HMAC over a given key-value pair (as for the Android SharedPreferences) or create an HMAC over a complete file that's provided by the file system.

When using an HMAC, you can use a bouncy castle implementation or the AndroidKeyStore to HMAC the given content.

Complete the following procedure when generating an HMAC with BouncyCastle:

  1. Make sure BouncyCastle or SpongyCastle is registered as a security provider.
  2. Initialize the HMAC with a key (which can be stored in a keystore).
  3. Get the byte array of the content that needs an HMAC.
  4. Call doFinal on the HMAC with the bytecode.
  5. Append the HMAC to the bytearray obtained in step 3.
  6. Store the result of step 5.

Complete the following procedure when verifying the HMAC with BouncyCastle:

  1. Make sure that BouncyCastle or SpongyCastle is registered as a security provider.
  2. Extract the message and the HMAC-bytes as separate arrays.
  3. Repeat steps 1-4 of the procedure for generating an HMAC.
  4. Compare the extracted HMAC-bytes to the result of step 3.

When generating the HMAC based on the Android Keystore, then it is best to only do this for Android 6.0 (API level 23) and higher.

The following is a convenient HMAC implementation without AndroidKeyStore:

public enum HMACWrapper {
    HMAC_512("HMac-SHA512"), //please note that this is the spec for the BC provider
    HMAC_256("HMac-SHA256");

    private final String algorithm;

    private HMACWrapper(final String algorithm) {
        this.algorithm = algorithm;
    }

    public Mac createHMAC(final SecretKey key) {
        try {
            Mac e = Mac.getInstance(this.algorithm, "BC");
            SecretKeySpec secret = new SecretKeySpec(key.getKey().getEncoded(), this.algorithm);
            e.init(secret);
            return e;
        } catch (NoSuchProviderException | InvalidKeyException | NoSuchAlgorithmException e) {
            //handle them
        }
    }

    public byte[] hmac(byte[] message, SecretKey key) {
        Mac mac = this.createHMAC(key);
        return mac.doFinal(message);
    }

    public boolean verify(byte[] messageWithHMAC, SecretKey key) {
        Mac mac = this.createHMAC(key);
        byte[] checksum = extractChecksum(messageWithHMAC, mac.getMacLength());
        byte[] message = extractMessage(messageWithHMAC, mac.getMacLength());
        byte[] calculatedChecksum = this.hmac(message, key);
        int diff = checksum.length ^ calculatedChecksum.length;

        for (int i = 0; i < checksum.length && i < calculatedChecksum.length; ++i) {
            diff |= checksum[i] ^ calculatedChecksum[i];
        }

        return diff == 0;
    }

    public byte[] extractMessage(byte[] messageWithHMAC) {
        Mac hmac = this.createHMAC(SecretKey.newKey());
        return extractMessage(messageWithHMAC, hmac.getMacLength());
    }

    private static byte[] extractMessage(byte[] body, int checksumLength) {
        if (body.length >= checksumLength) {
            byte[] message = new byte[body.length - checksumLength];
            System.arraycopy(body, 0, message, 0, message.length);
            return message;
        } else {
            return new byte[0];
        }
    }

    private static byte[] extractChecksum(byte[] body, int checksumLength) {
        if (body.length >= checksumLength) {
            byte[] checksum = new byte[checksumLength];
            System.arraycopy(body, body.length - checksumLength, checksum, 0, checksumLength);
            return checksum;
        } else {
            return new byte[0];
        }
    }

    static {
        Security.addProvider(new BouncyCastleProvider());
    }
}

Another way to provide integrity is to sign the byte array you obtained and add the signature to the original byte array.