From 117223ab79de54a5dd0268a732e15903cf19cd4e Mon Sep 17 00:00:00 2001 From: Khaled Abdelmohsen Date: Mon, 23 Dec 2019 18:32:11 +0000 Subject: [PATCH] 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 --- .../integrity/parser/RuleBinaryParser.java | 52 ++++++++--- .../serializer/RuleBinarySerializer.java | 39 +++++--- .../parser/RuleBinaryParserTest.java | 64 +++++++++---- .../serializer/RuleBinarySerializerTest.java | 90 +++++++++++++------ 4 files changed, 178 insertions(+), 67 deletions(-) diff --git a/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java b/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java index 3ef45a637bc19..8f84abc88752b 100644 --- a/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java +++ b/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java @@ -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 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; + } } diff --git a/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java b/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java index 73a815ad032b6..cb83765dc5d07 100644 --- a/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java +++ b/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java @@ -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 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); + } } diff --git a/services/tests/servicestests/src/com/android/server/integrity/parser/RuleBinaryParserTest.java b/services/tests/servicestests/src/com/android/server/integrity/parser/RuleBinaryParserTest.java index 88b6d70688ad7..86778f5303873 100644 --- a/services/tests/servicestests/src/com/android/server/integrity/parser/RuleBinaryParserTest.java +++ b/services/tests/servicestests/src/com/android/server/integrity/parser/RuleBinaryParserTest.java @@ -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 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; diff --git a/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java index 2304bc6dc6991..3c78c37f32baa 100644 --- a/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java +++ b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java @@ -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