Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 117223ab authored by Khaled Abdelmohsen's avatar Khaled Abdelmohsen
Browse files

Convert value from/to hex encoding

Values in atomic formulas could be represented as a hash. If the value
is hashed, it is represented in hex encoding. Binary parser and
serializer convert the value accordingly.

Bug: 143697198
Test: atest FrameworksServicesTests:RuleBinaryParserTest
Test: atest FrameworksServicesTests:RuleBinarySerializerTest
Change-Id: Ie10c4dcb0975c8d5768227ae224669f33cb16992
parent 7b36ffb0
Loading
Loading
Loading
Loading
+41 −11
Original line number Diff line number Diff line
@@ -34,16 +34,20 @@ import android.content.integrity.CompoundFormula;
import android.content.integrity.Formula;
import android.content.integrity.Rule;

import com.android.server.integrity.IntegrityUtils;
import com.android.server.integrity.model.BitInputStream;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

/** A helper class to parse rules into the {@link Rule} model from Binary representation. */
public class RuleBinaryParser implements RuleParser {

    private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();

    @Override
    public List<Rule> parse(byte[] ruleBytes) throws RuleParseException {
        try {
@@ -122,26 +126,52 @@ public class RuleBinaryParser implements RuleParser {
        int key = bitInputStream.getNext(KEY_BITS);
        int operator = bitInputStream.getNext(OPERATOR_BITS);

        boolean isHashedValue = bitInputStream.getNext(IS_HASHED_BITS) == 1;
        int valueSize = bitInputStream.getNext(VALUE_SIZE_BITS);
        StringBuilder value = new StringBuilder();
        while (valueSize-- > 0) {
            value.append((char) bitInputStream.getNext(/* numOfBits= */ 8));
        }

        switch (key) {
            case AtomicFormula.PACKAGE_NAME:
            case AtomicFormula.APP_CERTIFICATE:
            case AtomicFormula.INSTALLER_NAME:
            case AtomicFormula.INSTALLER_CERTIFICATE:
                return new AtomicFormula.StringAtomicFormula(key, value.toString(), isHashedValue);
                boolean isHashedValue = bitInputStream.getNext(IS_HASHED_BITS) == 1;
                int valueSize = bitInputStream.getNext(VALUE_SIZE_BITS);
                String stringValue = getStringValue(bitInputStream, valueSize, isHashedValue);
                return new AtomicFormula.StringAtomicFormula(key, stringValue, isHashedValue);
            case AtomicFormula.VERSION_CODE:
                return new AtomicFormula.IntAtomicFormula(
                        key, operator, Integer.parseInt(value.toString()));
                int intValue = getIntValue(bitInputStream);
                return new AtomicFormula.IntAtomicFormula(key, operator, intValue);
            case AtomicFormula.PRE_INSTALLED:
                return new AtomicFormula.BooleanAtomicFormula(key, value.toString().equals("1"));
                boolean booleanValue = getBooleanValue(bitInputStream);
                return new AtomicFormula.BooleanAtomicFormula(key, booleanValue);
            default:
                throw new IllegalArgumentException(String.format("Unknown key: %d", key));
        }
    }

    // Get value string from stream.
    // If the value is not hashed, get its raw form directly.
    // If the value is hashed, get the hex-encoding of the value. Serialized values are in raw form.
    // All hashed values are hex-encoded.
    private static String getStringValue(
            BitInputStream bitInputStream, int valueSize, boolean isHashedValue)
            throws IOException {
        if (!isHashedValue) {
            StringBuilder value = new StringBuilder();
            while (valueSize-- > 0) {
                value.append((char) bitInputStream.getNext(/* numOfBits= */ 8));
            }
            return value.toString();
        }
        ByteBuffer byteBuffer = ByteBuffer.allocate(valueSize);
        while (valueSize-- > 0) {
            byteBuffer.put((byte) (bitInputStream.getNext(/* numOfBits= */ 8) & 0xFF));
        }
        return IntegrityUtils.getHexDigest(byteBuffer.array());
    }

    private static int getIntValue(BitInputStream bitInputStream) throws IOException {
        return bitInputStream.getNext(/* numOfBits= */ 32);
    }

    private static boolean getBooleanValue(BitInputStream bitInputStream) throws IOException {
        return bitInputStream.getNext(/* numOfBits= */ 1) == 1;
    }
}
+28 −11
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import android.content.integrity.CompoundFormula;
import android.content.integrity.Formula;
import android.content.integrity.Rule;

import com.android.server.integrity.IntegrityUtils;
import com.android.server.integrity.model.BitOutputStream;

import java.io.ByteArrayOutputStream;
@@ -93,6 +94,9 @@ public class RuleBinarySerializer implements RuleSerializer {

    private void serializeIndexedRules(List<Rule> rules, OutputStream outputStream)
            throws IOException {
        if (rules == null) {
            return;
        }
        BitOutputStream bitOutputStream = new BitOutputStream();
        for (Rule rule : rules) {
            bitOutputStream.clear();
@@ -153,7 +157,7 @@ public class RuleBinarySerializer implements RuleSerializer {
            AtomicFormula.StringAtomicFormula stringAtomicFormula =
                    (AtomicFormula.StringAtomicFormula) atomicFormula;
            bitOutputStream.setNext(OPERATOR_BITS, AtomicFormula.EQ);
            serializeValue(
            serializeStringValue(
                    stringAtomicFormula.getValue(),
                    stringAtomicFormula.getIsHashedValue(),
                    bitOutputStream);
@@ -161,27 +165,21 @@ public class RuleBinarySerializer implements RuleSerializer {
            AtomicFormula.IntAtomicFormula intAtomicFormula =
                    (AtomicFormula.IntAtomicFormula) atomicFormula;
            bitOutputStream.setNext(OPERATOR_BITS, intAtomicFormula.getOperator());
            serializeValue(
                    String.valueOf(intAtomicFormula.getValue()),
                    /* isHashedValue= */ false,
                    bitOutputStream);
            serializeIntValue(intAtomicFormula.getValue(), bitOutputStream);
        } else if (atomicFormula.getTag() == AtomicFormula.BOOLEAN_ATOMIC_FORMULA_TAG) {
            AtomicFormula.BooleanAtomicFormula booleanAtomicFormula =
                    (AtomicFormula.BooleanAtomicFormula) atomicFormula;
            bitOutputStream.setNext(OPERATOR_BITS, AtomicFormula.EQ);
            serializeValue(
                    booleanAtomicFormula.getValue() ? "1" : "0",
                    /* isHashedValue= */ false,
                    bitOutputStream);
            serializeBooleanValue(booleanAtomicFormula.getValue(), bitOutputStream);
        } else {
            throw new IllegalArgumentException(
                    String.format("Invalid atomic formula type: %s", atomicFormula.getClass()));
        }
    }

    private void serializeValue(
    private void serializeStringValue(
            String value, boolean isHashedValue, BitOutputStream bitOutputStream) {
        byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
        byte[] valueBytes = getBytesForString(value, isHashedValue);

        bitOutputStream.setNext(isHashedValue);
        bitOutputStream.setNext(VALUE_SIZE_BITS, valueBytes.length);
@@ -189,4 +187,23 @@ public class RuleBinarySerializer implements RuleSerializer {
            bitOutputStream.setNext(/* numOfBits= */ 8, valueByte);
        }
    }

    private void serializeIntValue(int value, BitOutputStream bitOutputStream) {
        bitOutputStream.setNext(/* numOfBits= */ 32, value);
    }

    private void serializeBooleanValue(boolean value, BitOutputStream bitOutputStream) {
        bitOutputStream.setNext(value);
    }

    // Get the byte array for a value.
    // If the value is not hashed, use its byte array form directly.
    // If the value is hashed, get the raw form decoding of the value. All hashed values are
    // hex-encoded. Serialized values are in raw form.
    private static byte[] getBytesForString(String value, boolean isHashedValue) {
        if (!isHashedValue) {
            return value.getBytes(StandardCharsets.UTF_8);
        }
        return IntegrityUtils.getBytesFromHexDigest(value);
    }
}
+46 −18
Original line number Diff line number Diff line
@@ -38,6 +38,8 @@ import android.content.integrity.AtomicFormula;
import android.content.integrity.CompoundFormula;
import android.content.integrity.Rule;

import com.android.server.integrity.IntegrityUtils;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -45,6 +47,7 @@ import org.junit.runners.JUnit4;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -81,6 +84,7 @@ public class RuleBinaryParserTest {
    private static final String INVALID_OPERATOR = getBits(INVALID_OPERATOR_VALUE, OPERATOR_BITS);

    private static final String IS_NOT_HASHED = "0";
    private static final String IS_HASHED = "1";

    private static final String DENY = getBits(Rule.DENY, EFFECT_BITS);
    private static final int INVALID_EFFECT_VALUE = 5;
@@ -299,17 +303,48 @@ public class RuleBinaryParserTest {
        assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
    }

    @Test
    public void testBinaryString_validAtomicFormula_hashedValue() throws Exception {
        String appCertificate = "test_cert";
        String ruleBits =
                START_BIT
                        + ATOMIC_FORMULA_START_BITS
                        + APP_CERTIFICATE
                        + EQ
                        + IS_HASHED
                        + getBits(appCertificate.length(), VALUE_SIZE_BITS)
                        + getValueBits(appCertificate)
                        + DENY
                        + END_BIT;
        byte[] ruleBytes = getBytes(ruleBits);
        ByteBuffer rule =
                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
        rule.put(DEFAULT_FORMAT_VERSION_BYTES);
        rule.put(ruleBytes);
        RuleParser binaryParser = new RuleBinaryParser();
        Rule expectedRule =
                new Rule(
                        new AtomicFormula.StringAtomicFormula(
                                AtomicFormula.APP_CERTIFICATE,
                                IntegrityUtils.getHexDigest(
                                        appCertificate.getBytes(StandardCharsets.UTF_8)),
                                /* isHashedValue= */ true),
                        Rule.DENY);

        List<Rule> rules = binaryParser.parse(rule.array());

        assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
    }

    @Test
    public void testBinaryString_validAtomicFormula_integerValue() throws Exception {
        String versionCode = "1";
        int versionCode = 1;
        String ruleBits =
                START_BIT
                        + ATOMIC_FORMULA_START_BITS
                        + VERSION_CODE
                        + EQ
                        + IS_NOT_HASHED
                        + getBits(versionCode.length(), VALUE_SIZE_BITS)
                        + getValueBits(versionCode)
                        + getBits(versionCode, /* numOfBits= */ 32)
                        + DENY
                        + END_BIT;
        byte[] ruleBytes = getBytes(ruleBits);
@@ -337,9 +372,7 @@ public class RuleBinaryParserTest {
                        + ATOMIC_FORMULA_START_BITS
                        + PRE_INSTALLED
                        + EQ
                        + IS_NOT_HASHED
                        + getBits(isPreInstalled.length(), VALUE_SIZE_BITS)
                        + getValueBits(isPreInstalled)
                        + isPreInstalled
                        + DENY
                        + END_BIT;
        byte[] ruleBytes = getBytes(ruleBits);
@@ -360,17 +393,14 @@ public class RuleBinaryParserTest {

    @Test
    public void testBinaryString_invalidAtomicFormula() throws Exception {
        String versionCode = "test";
        int versionCode = 1;
        String ruleBits =
                START_BIT
                        + ATOMIC_FORMULA_START_BITS
                        + VERSION_CODE
                        + EQ
                        + IS_NOT_HASHED
                        + getBits(versionCode.length(), VALUE_SIZE_BITS)
                        + getValueBits(versionCode)
                        + DENY
                        + END_BIT;
                        + getBits(versionCode, /* numOfBits= */ 32)
                        + DENY;
        byte[] ruleBytes = getBytes(ruleBits);
        ByteBuffer rule =
                ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
@@ -380,7 +410,7 @@ public class RuleBinaryParserTest {

        assertExpectException(
                RuleParseException.class,
                /* expectedExceptionMessageRegex */ "For input string:",
                /* expectedExceptionMessageRegex */ "A rule must end with a '1' bit.",
                () -> binaryParser.parse(rule.array()));
    }

@@ -449,7 +479,7 @@ public class RuleBinaryParserTest {

    @Test
    public void testBinaryString_invalidRule_invalidOperator() throws Exception {
        String versionCode = "1";
        int versionCode = 1;
        String ruleBits =
                START_BIT
                        + COMPOUND_FORMULA_START_BITS
@@ -457,9 +487,7 @@ public class RuleBinaryParserTest {
                        + ATOMIC_FORMULA_START_BITS
                        + VERSION_CODE
                        + INVALID_OPERATOR
                        + IS_NOT_HASHED
                        + getBits(versionCode.length(), VALUE_SIZE_BITS)
                        + getValueBits(versionCode)
                        + getBits(versionCode, /* numOfBits= */ 32)
                        + COMPOUND_FORMULA_END_BITS
                        + DENY
                        + END_BIT;
+63 −27
Original line number Diff line number Diff line
@@ -42,11 +42,14 @@ import android.content.integrity.Rule;

import androidx.annotation.NonNull;

import com.android.server.integrity.IntegrityUtils;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -81,6 +84,7 @@ public class RuleBinarySerializerTest {
    private static final String EQ = getBits(AtomicFormula.EQ, OPERATOR_BITS);

    private static final String IS_NOT_HASHED = "0";
    private static final String IS_HASHED = "1";

    private static final String DENY = getBits(Rule.DENY, EFFECT_BITS);

@@ -96,10 +100,9 @@ public class RuleBinarySerializerTest {

        assertExpectException(
                RuleSerializeException.class,
                /* expectedExceptionMessageRegex= */
                "Index buckets cannot be created for null rule list.",
                () ->
                        binarySerializer.serialize(null, /* formatVersion= */ Optional.empty()));
                /* expectedExceptionMessageRegex= */ "Index buckets cannot be created for null"
                        + " rule list.",
                () -> binarySerializer.serialize(null, /* formatVersion= */ Optional.empty()));
    }

    @Test
@@ -329,15 +332,47 @@ public class RuleBinarySerializerTest {
        assertThat(actualRules).isEqualTo(expectedRules);
    }

    @Test
    public void testBinaryString_serializeValidAtomicFormula_hashedValue() throws Exception {
        String appCertificate = "test_cert";
        Rule rule =
                new Rule(
                        new AtomicFormula.StringAtomicFormula(
                                AtomicFormula.APP_CERTIFICATE,
                                IntegrityUtils.getHexDigest(
                                        appCertificate.getBytes(StandardCharsets.UTF_8)),
                                /* isHashedValue= */ true),
                        Rule.DENY);
        RuleSerializer binarySerializer = new RuleBinarySerializer();
        String expectedBits =
                START_BIT
                        + ATOMIC_FORMULA_START_BITS
                        + APP_CERTIFICATE
                        + EQ
                        + IS_HASHED
                        + getBits(appCertificate.length(), VALUE_SIZE_BITS)
                        + getValueBits(appCertificate)
                        + DENY
                        + END_BIT;
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
        byteArrayOutputStream.write(getBytes(expectedBits));
        byte[] expectedRules = byteArrayOutputStream.toByteArray();

        byte[] actualRules =
                binarySerializer.serialize(
                        Collections.singletonList(rule), /* formatVersion= */ Optional.empty());

        assertThat(actualRules).isEqualTo(expectedRules);
    }

    @Test
    public void testBinaryString_serializeValidAtomicFormula_integerValue() throws Exception {
        String versionCode = "1";
        int versionCode = 1;
        Rule rule =
                new Rule(
                        new AtomicFormula.IntAtomicFormula(
                                AtomicFormula.VERSION_CODE,
                                AtomicFormula.EQ,
                                Integer.parseInt(versionCode)),
                                AtomicFormula.VERSION_CODE, AtomicFormula.EQ, versionCode),
                        Rule.DENY);
        RuleSerializer binarySerializer = new RuleBinarySerializer();
        String expectedBits =
@@ -345,9 +380,7 @@ public class RuleBinarySerializerTest {
                        + ATOMIC_FORMULA_START_BITS
                        + VERSION_CODE
                        + EQ
                        + IS_NOT_HASHED
                        + getBits(versionCode.length(), VALUE_SIZE_BITS)
                        + getValueBits(versionCode)
                        + getBits(versionCode, /* numOfBits= */ 32)
                        + DENY
                        + END_BIT;
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
@@ -375,9 +408,7 @@ public class RuleBinarySerializerTest {
                        + ATOMIC_FORMULA_START_BITS
                        + PRE_INSTALLED
                        + EQ
                        + IS_NOT_HASHED
                        + getBits(preInstalled.length(), VALUE_SIZE_BITS)
                        + getValueBits(preInstalled)
                        + preInstalled
                        + DENY
                        + END_BIT;
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
@@ -457,28 +488,33 @@ public class RuleBinarySerializerTest {
        byte[] actualRules =
                binarySerializer.serialize(ruleList, /* formatVersion= */ Optional.empty());


        // Note that ordering is important here and the test verifies that the rules are written
        // in this sorted order.
        ByteArrayOutputStream expectedArrayOutputStream = new ByteArrayOutputStream();
        expectedArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
        expectedArrayOutputStream.write(
                getBytes(getSerializedCompoundRuleWithPackageNameAndSampleInstallerName(
                getBytes(
                        getSerializedCompoundRuleWithPackageNameAndSampleInstallerName(
                                packageNameA)));
        expectedArrayOutputStream.write(
                getBytes(getSerializedCompoundRuleWithPackageNameAndSampleInstallerName(
                getBytes(
                        getSerializedCompoundRuleWithPackageNameAndSampleInstallerName(
                                packageNameB)));
        expectedArrayOutputStream.write(
                getBytes(getSerializedCompoundRuleWithPackageNameAndSampleInstallerName(
                getBytes(
                        getSerializedCompoundRuleWithPackageNameAndSampleInstallerName(
                                packageNameC)));
        expectedArrayOutputStream.write(
                getBytes(getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
                getBytes(
                        getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
                                appCert1)));
        expectedArrayOutputStream.write(
                getBytes(getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
                getBytes(
                        getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
                                appCert2)));
        expectedArrayOutputStream.write(
                getBytes(getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
                getBytes(
                        getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
                                appCert3)));
        String expectedBitsForInstallerRule =
                START_BIT