diff --git a/services/core/java/com/android/server/integrity/model/BitTrackedInputStream.java b/services/core/java/com/android/server/integrity/model/BitTrackedInputStream.java new file mode 100644 index 0000000000000..e555e3e5746e3 --- /dev/null +++ b/services/core/java/com/android/server/integrity/model/BitTrackedInputStream.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.integrity.model; + +import java.io.IOException; +import java.io.InputStream; + +/** + * An input stream that tracks the total number read bytes since construction and allows moving + * fast forward to a certain byte any time during the execution. + * + * This class is used for efficient reading of rules based on the rule indexing. + */ +public class BitTrackedInputStream extends BitInputStream { + + private static int sReadBitsCount; + + /** Constructor with byte array. */ + public BitTrackedInputStream(byte[] inputStream) { + super(inputStream); + sReadBitsCount = 0; + } + + /** Constructor with input stream. */ + public BitTrackedInputStream(InputStream inputStream) { + super(inputStream); + sReadBitsCount = 0; + } + + /** Obtains an integer value of the next {@code numOfBits}. */ + @Override + public int getNext(int numOfBits) throws IOException { + sReadBitsCount += numOfBits; + return super.getNext(numOfBits); + } + + /** Returns the current cursor position showing the number of bits that are read. */ + public int getReadBitsCount() { + return sReadBitsCount; + } + + /** + * Sets the cursor to the specified byte location. + * + * Note that the integer parameter specifies the location in bytes -- not bits. + */ + public void setCursorToByteLocation(int byteLocation) throws IOException { + int bitCountToRead = byteLocation * 8 - sReadBitsCount; + if (bitCountToRead < 0) { + throw new IllegalStateException("The byte position is already read."); + } + super.getNext(bitCountToRead); + sReadBitsCount = byteLocation * 8; + } +} diff --git a/services/core/java/com/android/server/integrity/serializer/ByteTrackedOutputStream.java b/services/core/java/com/android/server/integrity/model/ByteTrackedOutputStream.java similarity index 97% rename from services/core/java/com/android/server/integrity/serializer/ByteTrackedOutputStream.java rename to services/core/java/com/android/server/integrity/model/ByteTrackedOutputStream.java index 62815a9806eeb..f575599e1c49e 100644 --- a/services/core/java/com/android/server/integrity/serializer/ByteTrackedOutputStream.java +++ b/services/core/java/com/android/server/integrity/model/ByteTrackedOutputStream.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.integrity.serializer; +package com.android.server.integrity.model; import java.io.IOException; import java.io.OutputStream; 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 cbb6e4e8e06f8..1d426fe51a9c7 100644 --- a/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java +++ b/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java @@ -37,7 +37,7 @@ import android.content.integrity.CompoundFormula; import android.content.integrity.Formula; import android.content.integrity.Rule; -import com.android.server.integrity.model.BitInputStream; +import com.android.server.integrity.model.BitTrackedInputStream; import java.io.IOException; import java.io.InputStream; @@ -47,13 +47,11 @@ 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 { - BitInputStream bitInputStream = new BitInputStream(ruleBytes); - return parseRules(bitInputStream); + BitTrackedInputStream bitTrackedInputStream = new BitTrackedInputStream(ruleBytes); + return parseRules(bitTrackedInputStream); } catch (Exception e) { throw new RuleParseException(e.getMessage(), e); } @@ -62,46 +60,46 @@ public class RuleBinaryParser implements RuleParser { @Override public List parse(InputStream inputStream) throws RuleParseException { try { - BitInputStream bitInputStream = new BitInputStream(inputStream); - return parseRules(bitInputStream); + BitTrackedInputStream bitTrackedInputStream = new BitTrackedInputStream(inputStream); + return parseRules(bitTrackedInputStream); } catch (Exception e) { throw new RuleParseException(e.getMessage(), e); } } - private List parseRules(BitInputStream bitInputStream) throws IOException { + private List parseRules(BitTrackedInputStream bitTrackedInputStream) throws IOException { List parsedRules = new ArrayList<>(); // Read the rule binary file format version. - bitInputStream.getNext(FORMAT_VERSION_BITS); + bitTrackedInputStream.getNext(FORMAT_VERSION_BITS); - while (bitInputStream.hasNext()) { - if (bitInputStream.getNext(SIGNAL_BIT) == 1) { - parsedRules.add(parseRule(bitInputStream)); + while (bitTrackedInputStream.hasNext()) { + if (bitTrackedInputStream.getNext(SIGNAL_BIT) == 1) { + parsedRules.add(parseRule(bitTrackedInputStream)); } } return parsedRules; } - private Rule parseRule(BitInputStream bitInputStream) throws IOException { - Formula formula = parseFormula(bitInputStream); - int effect = bitInputStream.getNext(EFFECT_BITS); + private Rule parseRule(BitTrackedInputStream bitTrackedInputStream) throws IOException { + Formula formula = parseFormula(bitTrackedInputStream); + int effect = bitTrackedInputStream.getNext(EFFECT_BITS); - if (bitInputStream.getNext(SIGNAL_BIT) != 1) { + if (bitTrackedInputStream.getNext(SIGNAL_BIT) != 1) { throw new IllegalArgumentException("A rule must end with a '1' bit."); } return new Rule(formula, effect); } - private Formula parseFormula(BitInputStream bitInputStream) throws IOException { - int separator = bitInputStream.getNext(SEPARATOR_BITS); + private Formula parseFormula(BitTrackedInputStream bitTrackedInputStream) throws IOException { + int separator = bitTrackedInputStream.getNext(SEPARATOR_BITS); switch (separator) { case ATOMIC_FORMULA_START: - return parseAtomicFormula(bitInputStream); + return parseAtomicFormula(bitTrackedInputStream); case COMPOUND_FORMULA_START: - return parseCompoundFormula(bitInputStream); + return parseCompoundFormula(bitTrackedInputStream); case COMPOUND_FORMULA_END: return null; default: @@ -110,37 +108,40 @@ public class RuleBinaryParser implements RuleParser { } } - private CompoundFormula parseCompoundFormula(BitInputStream bitInputStream) throws IOException { - int connector = bitInputStream.getNext(CONNECTOR_BITS); + private CompoundFormula parseCompoundFormula(BitTrackedInputStream bitTrackedInputStream) + throws IOException { + int connector = bitTrackedInputStream.getNext(CONNECTOR_BITS); List formulas = new ArrayList<>(); - Formula parsedFormula = parseFormula(bitInputStream); + Formula parsedFormula = parseFormula(bitTrackedInputStream); while (parsedFormula != null) { formulas.add(parsedFormula); - parsedFormula = parseFormula(bitInputStream); + parsedFormula = parseFormula(bitTrackedInputStream); } return new CompoundFormula(connector, formulas); } - private AtomicFormula parseAtomicFormula(BitInputStream bitInputStream) throws IOException { - int key = bitInputStream.getNext(KEY_BITS); - int operator = bitInputStream.getNext(OPERATOR_BITS); + private AtomicFormula parseAtomicFormula(BitTrackedInputStream bitTrackedInputStream) + throws IOException { + int key = bitTrackedInputStream.getNext(KEY_BITS); + int operator = bitTrackedInputStream.getNext(OPERATOR_BITS); switch (key) { case AtomicFormula.PACKAGE_NAME: case AtomicFormula.APP_CERTIFICATE: case AtomicFormula.INSTALLER_NAME: case AtomicFormula.INSTALLER_CERTIFICATE: - boolean isHashedValue = bitInputStream.getNext(IS_HASHED_BITS) == 1; - int valueSize = bitInputStream.getNext(VALUE_SIZE_BITS); - String stringValue = getStringValue(bitInputStream, valueSize, isHashedValue); + boolean isHashedValue = bitTrackedInputStream.getNext(IS_HASHED_BITS) == 1; + int valueSize = bitTrackedInputStream.getNext(VALUE_SIZE_BITS); + String stringValue = getStringValue(bitTrackedInputStream, valueSize, + isHashedValue); return new AtomicFormula.StringAtomicFormula(key, stringValue, isHashedValue); case AtomicFormula.VERSION_CODE: - int intValue = getIntValue(bitInputStream); + int intValue = getIntValue(bitTrackedInputStream); return new AtomicFormula.IntAtomicFormula(key, operator, intValue); case AtomicFormula.PRE_INSTALLED: - boolean booleanValue = getBooleanValue(bitInputStream); + boolean booleanValue = getBooleanValue(bitTrackedInputStream); return new AtomicFormula.BooleanAtomicFormula(key, booleanValue); default: throw new IllegalArgumentException(String.format("Unknown key: %d", key)); 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 b8791c3c34893..f964d4cf27242 100644 --- a/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java +++ b/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java @@ -42,6 +42,7 @@ import android.content.integrity.Rule; import com.android.internal.util.Preconditions; import com.android.server.integrity.IntegrityUtils; import com.android.server.integrity.model.BitOutputStream; +import com.android.server.integrity.model.ByteTrackedOutputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -109,7 +110,8 @@ public class RuleBinarySerializer implements RuleSerializer { } private void serializeRuleFileMetadata(Optional formatVersion, - ByteTrackedOutputStream outputStream) throws IOException { + ByteTrackedOutputStream outputStream) + throws IOException { int formatVersionValue = formatVersion.orElse(DEFAULT_FORMAT_VERSION); BitOutputStream bitOutputStream = new BitOutputStream(); diff --git a/services/tests/servicestests/src/com/android/server/integrity/model/BitTrackedInputStreamTest.java b/services/tests/servicestests/src/com/android/server/integrity/model/BitTrackedInputStreamTest.java new file mode 100644 index 0000000000000..1eb5eb51504af --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/integrity/model/BitTrackedInputStreamTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.integrity.model; + +import static com.android.server.integrity.model.ComponentBitSize.ATOMIC_FORMULA_START; +import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_END; +import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_START; +import static com.android.server.integrity.model.ComponentBitSize.CONNECTOR_BITS; +import static com.android.server.integrity.model.ComponentBitSize.EFFECT_BITS; +import static com.android.server.integrity.model.ComponentBitSize.KEY_BITS; +import static com.android.server.integrity.model.ComponentBitSize.OPERATOR_BITS; +import static com.android.server.integrity.model.ComponentBitSize.SEPARATOR_BITS; +import static com.android.server.integrity.model.ComponentBitSize.VALUE_SIZE_BITS; +import static com.android.server.integrity.utils.TestUtils.getBits; +import static com.android.server.integrity.utils.TestUtils.getBytes; +import static com.android.server.integrity.utils.TestUtils.getValueBits; + +import static com.google.common.truth.Truth.assertThat; + +import static org.testng.Assert.assertThrows; + +import android.content.integrity.AtomicFormula; +import android.content.integrity.CompoundFormula; +import android.content.integrity.Rule; + +import com.android.server.integrity.parser.BinaryFileOperations; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.IOException; + +@RunWith(JUnit4.class) +public class BitTrackedInputStreamTest { + private static final String COMPOUND_FORMULA_START_BITS = + getBits(COMPOUND_FORMULA_START, SEPARATOR_BITS); + private static final String COMPOUND_FORMULA_END_BITS = + getBits(COMPOUND_FORMULA_END, SEPARATOR_BITS); + private static final String ATOMIC_FORMULA_START_BITS = + getBits(ATOMIC_FORMULA_START, SEPARATOR_BITS); + private static final String NOT = getBits(CompoundFormula.NOT, CONNECTOR_BITS); + private static final String PACKAGE_NAME = getBits(AtomicFormula.PACKAGE_NAME, KEY_BITS); + private static final String EQ = getBits(AtomicFormula.EQ, OPERATOR_BITS); + private static final String DENY = getBits(Rule.DENY, EFFECT_BITS); + + private static final String IS_NOT_HASHED = "0"; + private static final String START_BIT = "1"; + private static final String END_BIT = "1"; + + @Test + public void testBitOperationsCountBitsCorrectly() throws IOException { + String packageName = "com.test.app"; + byte[] testInput = + getBytes( + START_BIT + + COMPOUND_FORMULA_START_BITS + + NOT + + ATOMIC_FORMULA_START_BITS + + PACKAGE_NAME + + EQ + + IS_NOT_HASHED + + getBits(packageName.length(), VALUE_SIZE_BITS) + + getValueBits(packageName) + + COMPOUND_FORMULA_END_BITS + + DENY + + END_BIT); + + BitTrackedInputStream bitTrackedInputStream = new BitTrackedInputStream(testInput); + + // Right after construction, the read bits count should be 0. + assertThat(bitTrackedInputStream.getReadBitsCount()).isEqualTo(0); + + // Get next 10 bits should result with 10 bits read. + bitTrackedInputStream.getNext(10); + assertThat(bitTrackedInputStream.getReadBitsCount()).isEqualTo(10); + + // When we move the cursor 8 bytes, we should point to 64 bits. + bitTrackedInputStream.setCursorToByteLocation(8); + assertThat(bitTrackedInputStream.getReadBitsCount()).isEqualTo(64); + + // Read until the end and the total size of the input stream should be available. + while (bitTrackedInputStream.hasNext()) { + bitTrackedInputStream.getNext(1); + } + assertThat(bitTrackedInputStream.getReadBitsCount()).isEqualTo(128); + } + + @Test + public void testBitInputStreamOperationsStillWork() throws IOException { + String packageName = "com.test.app"; + byte[] testInput = + getBytes( + IS_NOT_HASHED + + getBits(packageName.length(), VALUE_SIZE_BITS) + + getValueBits(packageName)); + + BitTrackedInputStream bitTrackedInputStream = new BitTrackedInputStream(testInput); + assertThat(bitTrackedInputStream.getReadBitsCount()).isEqualTo(0); + + // Read until the string parameter. + String stringValue = BinaryFileOperations.getStringValue(bitTrackedInputStream); + + // Verify that the read bytes are counted. + assertThat(stringValue).isEqualTo(packageName); + assertThat(bitTrackedInputStream.getReadBitsCount()).isGreaterThan(0); + } + + @Test + public void testBitTrackedInputStream_moveCursorForwardFailsIfAlreadyRead() throws IOException { + String packageName = "com.test.app"; + byte[] testInput = + getBytes( + IS_NOT_HASHED + + getBits(packageName.length(), VALUE_SIZE_BITS) + + getValueBits(packageName)); + + BitTrackedInputStream bitTrackedInputStream = new BitTrackedInputStream(testInput); + + // Read more than two bytes. + bitTrackedInputStream.getNext(20); + + // Ask to move the cursor to the second byte. + assertThrows( + IllegalStateException.class, + () -> bitTrackedInputStream.setCursorToByteLocation(2)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/integrity/serializer/ByteTrackedOutputStreamTest.java b/services/tests/servicestests/src/com/android/server/integrity/model/ByteTrackedOutputStreamTest.java similarity index 96% rename from services/tests/servicestests/src/com/android/server/integrity/serializer/ByteTrackedOutputStreamTest.java rename to services/tests/servicestests/src/com/android/server/integrity/model/ByteTrackedOutputStreamTest.java index 5ecb8b5c81695..c7cc343dd77e0 100644 --- a/services/tests/servicestests/src/com/android/server/integrity/serializer/ByteTrackedOutputStreamTest.java +++ b/services/tests/servicestests/src/com/android/server/integrity/model/ByteTrackedOutputStreamTest.java @@ -14,12 +14,10 @@ * limitations under the License. */ -package com.android.server.integrity.serializer; +package com.android.server.integrity.model; import static com.google.common.truth.Truth.assertThat; -import com.android.server.integrity.model.BitOutputStream; - import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4;