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
This commit is contained in:
Khaled Abdelmohsen
2019-12-23 18:32:11 +00:00
parent 7b36ffb06c
commit 117223ab79
4 changed files with 178 additions and 67 deletions

View File

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

View File

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

View File

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

View File

@@ -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,29 +488,34 @@ 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(
packageNameA)));
getBytes(
getSerializedCompoundRuleWithPackageNameAndSampleInstallerName(
packageNameA)));
expectedArrayOutputStream.write(
getBytes(getSerializedCompoundRuleWithPackageNameAndSampleInstallerName(
packageNameB)));
getBytes(
getSerializedCompoundRuleWithPackageNameAndSampleInstallerName(
packageNameB)));
expectedArrayOutputStream.write(
getBytes(getSerializedCompoundRuleWithPackageNameAndSampleInstallerName(
packageNameC)));
getBytes(
getSerializedCompoundRuleWithPackageNameAndSampleInstallerName(
packageNameC)));
expectedArrayOutputStream.write(
getBytes(getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
appCert1)));
getBytes(
getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
appCert1)));
expectedArrayOutputStream.write(
getBytes(getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
appCert2)));
getBytes(
getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
appCert2)));
expectedArrayOutputStream.write(
getBytes(getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
appCert3)));
getBytes(
getSerializedCompoundRuleWithCertificateNameAndSampleInstallerName(
appCert3)));
String expectedBitsForInstallerRule =
START_BIT
+ COMPOUND_FORMULA_START_BITS