| /* |
| * Copyright (C) 2017 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.apksig.internal.asn1; |
| |
| import com.android.apksig.internal.asn1.ber.BerDataValue; |
| import com.android.apksig.internal.asn1.ber.BerDataValueFormatException; |
| import com.android.apksig.internal.asn1.ber.BerDataValueReader; |
| import com.android.apksig.internal.asn1.ber.BerEncoding; |
| import com.android.apksig.internal.asn1.ber.ByteBufferBerDataValueReader; |
| import com.android.apksig.internal.util.ByteBufferUtils; |
| |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import java.math.BigInteger; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| /** |
| * Parser of ASN.1 BER-encoded structures. |
| * |
| * <p>Structure is described to the parser by providing a class annotated with {@link Asn1Class}, |
| * containing fields annotated with {@link Asn1Field}. |
| */ |
| public final class Asn1BerParser { |
| private Asn1BerParser() {} |
| |
| /** |
| * Returns the ASN.1 structure contained in the BER encoded input. |
| * |
| * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer |
| * is advanced to the first position following the end of the consumed structure. |
| * @param containerClass class describing the structure of the input. The class must meet the |
| * following requirements: |
| * <ul> |
| * <li>The class must be annotated with {@link Asn1Class}.</li> |
| * <li>The class must expose a public no-arg constructor.</li> |
| * <li>Member fields of the class which are populated with parsed input must be |
| * annotated with {@link Asn1Field} and be public and non-final.</li> |
| * </ul> |
| * |
| * @throws Asn1DecodingException if the input could not be decoded into the specified Java |
| * object |
| */ |
| public static <T> T parse(ByteBuffer encoded, Class<T> containerClass) |
| throws Asn1DecodingException { |
| BerDataValue containerDataValue; |
| try { |
| containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue(); |
| } catch (BerDataValueFormatException e) { |
| throw new Asn1DecodingException("Failed to decode top-level data value", e); |
| } |
| if (containerDataValue == null) { |
| throw new Asn1DecodingException("Empty input"); |
| } |
| return parse(containerDataValue, containerClass); |
| } |
| |
| /** |
| * Returns the implicit {@code SET OF} contained in the provided ASN.1 BER input. Implicit means |
| * that this method does not care whether the tag number of this data structure is |
| * {@code SET OF} and whether the tag class is {@code UNIVERSAL}. |
| * |
| * <p>Note: The returned type is {@link List} rather than {@link Set} because ASN.1 SET may |
| * contain duplicate elements. |
| * |
| * @param encoded encoded input. If the decoding operation succeeds, the position of this buffer |
| * is advanced to the first position following the end of the consumed structure. |
| * @param elementClass class describing the structure of the values/elements contained in this |
| * container. The class must meet the following requirements: |
| * <ul> |
| * <li>The class must be annotated with {@link Asn1Class}.</li> |
| * <li>The class must expose a public no-arg constructor.</li> |
| * <li>Member fields of the class which are populated with parsed input must be |
| * annotated with {@link Asn1Field} and be public and non-final.</li> |
| * </ul> |
| * |
| * @throws Asn1DecodingException if the input could not be decoded into the specified Java |
| * object |
| */ |
| public static <T> List<T> parseImplicitSetOf(ByteBuffer encoded, Class<T> elementClass) |
| throws Asn1DecodingException { |
| BerDataValue containerDataValue; |
| try { |
| containerDataValue = new ByteBufferBerDataValueReader(encoded).readDataValue(); |
| } catch (BerDataValueFormatException e) { |
| throw new Asn1DecodingException("Failed to decode top-level data value", e); |
| } |
| if (containerDataValue == null) { |
| throw new Asn1DecodingException("Empty input"); |
| } |
| return parseSetOf(containerDataValue, elementClass); |
| } |
| |
| private static <T> T parse(BerDataValue container, Class<T> containerClass) |
| throws Asn1DecodingException { |
| if (container == null) { |
| throw new NullPointerException("container == null"); |
| } |
| if (containerClass == null) { |
| throw new NullPointerException("containerClass == null"); |
| } |
| |
| Asn1Type dataType = getContainerAsn1Type(containerClass); |
| switch (dataType) { |
| case CHOICE: |
| return parseChoice(container, containerClass); |
| |
| case SEQUENCE: |
| { |
| int expectedTagClass = BerEncoding.TAG_CLASS_UNIVERSAL; |
| int expectedTagNumber = BerEncoding.getTagNumber(dataType); |
| if ((container.getTagClass() != expectedTagClass) |
| || (container.getTagNumber() != expectedTagNumber)) { |
| throw new Asn1UnexpectedTagException( |
| "Unexpected data value read as " + containerClass.getName() |
| + ". Expected " + BerEncoding.tagClassAndNumberToString( |
| expectedTagClass, expectedTagNumber) |
| + ", but read: " + BerEncoding.tagClassAndNumberToString( |
| container.getTagClass(), container.getTagNumber())); |
| } |
| return parseSequence(container, containerClass); |
| } |
| |
| default: |
| throw new Asn1DecodingException("Parsing container " + dataType + " not supported"); |
| } |
| } |
| |
| private static <T> T parseChoice(BerDataValue dataValue, Class<T> containerClass) |
| throws Asn1DecodingException { |
| List<AnnotatedField> fields = getAnnotatedFields(containerClass); |
| if (fields.isEmpty()) { |
| throw new Asn1DecodingException( |
| "No fields annotated with " + Asn1Field.class.getName() |
| + " in CHOICE class " + containerClass.getName()); |
| } |
| |
| // Check that class + tagNumber don't clash between the choices |
| for (int i = 0; i < fields.size() - 1; i++) { |
| AnnotatedField f1 = fields.get(i); |
| int tagNumber1 = f1.getBerTagNumber(); |
| int tagClass1 = f1.getBerTagClass(); |
| for (int j = i + 1; j < fields.size(); j++) { |
| AnnotatedField f2 = fields.get(j); |
| int tagNumber2 = f2.getBerTagNumber(); |
| int tagClass2 = f2.getBerTagClass(); |
| if ((tagNumber1 == tagNumber2) && (tagClass1 == tagClass2)) { |
| throw new Asn1DecodingException( |
| "CHOICE fields are indistinguishable because they have the same tag" |
| + " class and number: " + containerClass.getName() |
| + "." + f1.getField().getName() |
| + " and ." + f2.getField().getName()); |
| } |
| } |
| } |
| |
| // Instantiate the container object / result |
| T obj; |
| try { |
| obj = containerClass.getConstructor().newInstance(); |
| } catch (IllegalArgumentException | ReflectiveOperationException e) { |
| throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e); |
| } |
| |
| // Set the matching field's value from the data value |
| for (AnnotatedField field : fields) { |
| try { |
| field.setValueFrom(dataValue, obj); |
| return obj; |
| } catch (Asn1UnexpectedTagException expected) { |
| // not a match |
| } |
| } |
| |
| throw new Asn1DecodingException( |
| "No options of CHOICE " + containerClass.getName() + " matched"); |
| } |
| |
| private static <T> T parseSequence(BerDataValue container, Class<T> containerClass) |
| throws Asn1DecodingException { |
| List<AnnotatedField> fields = getAnnotatedFields(containerClass); |
| Collections.sort( |
| fields, (f1, f2) -> f1.getAnnotation().index() - f2.getAnnotation().index()); |
| // Check that there are no fields with the same index |
| if (fields.size() > 1) { |
| AnnotatedField lastField = null; |
| for (AnnotatedField field : fields) { |
| if ((lastField != null) |
| && (lastField.getAnnotation().index() == field.getAnnotation().index())) { |
| throw new Asn1DecodingException( |
| "Fields have the same index: " + containerClass.getName() |
| + "." + lastField.getField().getName() |
| + " and ." + field.getField().getName()); |
| } |
| lastField = field; |
| } |
| } |
| |
| // Instantiate the container object / result |
| T t; |
| try { |
| t = containerClass.getConstructor().newInstance(); |
| } catch (IllegalArgumentException | ReflectiveOperationException e) { |
| throw new Asn1DecodingException("Failed to instantiate " + containerClass.getName(), e); |
| } |
| |
| // Parse fields one by one. A complication is that there may be optional fields. |
| int nextUnreadFieldIndex = 0; |
| BerDataValueReader elementsReader = container.contentsReader(); |
| while (nextUnreadFieldIndex < fields.size()) { |
| BerDataValue dataValue; |
| try { |
| dataValue = elementsReader.readDataValue(); |
| } catch (BerDataValueFormatException e) { |
| throw new Asn1DecodingException("Malformed data value", e); |
| } |
| if (dataValue == null) { |
| break; |
| } |
| |
| for (int i = nextUnreadFieldIndex; i < fields.size(); i++) { |
| AnnotatedField field = fields.get(i); |
| try { |
| if (field.isOptional()) { |
| // Optional field -- might not be present and we may thus be trying to set |
| // it from the wrong tag. |
| try { |
| field.setValueFrom(dataValue, t); |
| nextUnreadFieldIndex = i + 1; |
| break; |
| } catch (Asn1UnexpectedTagException e) { |
| // This field is not present, attempt to use this data value for the |
| // next / iteration of the loop |
| continue; |
| } |
| } else { |
| // Mandatory field -- if we can't set its value from this data value, then |
| // it's an error |
| field.setValueFrom(dataValue, t); |
| nextUnreadFieldIndex = i + 1; |
| break; |
| } |
| } catch (Asn1DecodingException e) { |
| throw new Asn1DecodingException( |
| "Failed to parse " + containerClass.getName() |
| + "." + field.getField().getName(), |
| e); |
| } |
| } |
| } |
| |
| return t; |
| } |
| |
| // NOTE: This method returns List rather than Set because ASN.1 SET_OF does require uniqueness |
| // of elements -- it's an unordered collection. |
| @SuppressWarnings("unchecked") |
| private static <T> List<T> parseSetOf(BerDataValue container, Class<T> elementClass) |
| throws Asn1DecodingException { |
| List<T> result = new ArrayList<>(); |
| BerDataValueReader elementsReader = container.contentsReader(); |
| while (true) { |
| BerDataValue dataValue; |
| try { |
| dataValue = elementsReader.readDataValue(); |
| } catch (BerDataValueFormatException e) { |
| throw new Asn1DecodingException("Malformed data value", e); |
| } |
| if (dataValue == null) { |
| break; |
| } |
| T element; |
| if (ByteBuffer.class.equals(elementClass)) { |
| element = (T) dataValue.getEncodedContents(); |
| } else if (Asn1OpaqueObject.class.equals(elementClass)) { |
| element = (T) new Asn1OpaqueObject(dataValue.getEncoded()); |
| } else { |
| element = parse(dataValue, elementClass); |
| } |
| result.add(element); |
| } |
| return result; |
| } |
| |
| private static Asn1Type getContainerAsn1Type(Class<?> containerClass) |
| throws Asn1DecodingException { |
| Asn1Class containerAnnotation = containerClass.getDeclaredAnnotation(Asn1Class.class); |
| if (containerAnnotation == null) { |
| throw new Asn1DecodingException( |
| containerClass.getName() + " is not annotated with " |
| + Asn1Class.class.getName()); |
| } |
| |
| switch (containerAnnotation.type()) { |
| case CHOICE: |
| case SEQUENCE: |
| return containerAnnotation.type(); |
| default: |
| throw new Asn1DecodingException( |
| "Unsupported ASN.1 container annotation type: " |
| + containerAnnotation.type()); |
| } |
| } |
| |
| private static Class<?> getElementType(Field field) |
| throws Asn1DecodingException, ClassNotFoundException { |
| String type = field.getGenericType().getTypeName(); |
| int delimiterIndex = type.indexOf('<'); |
| if (delimiterIndex == -1) { |
| throw new Asn1DecodingException("Not a container type: " + field.getGenericType()); |
| } |
| int startIndex = delimiterIndex + 1; |
| int endIndex = type.indexOf('>', startIndex); |
| // TODO: handle comma? |
| if (endIndex == -1) { |
| throw new Asn1DecodingException("Not a container type: " + field.getGenericType()); |
| } |
| String elementClassName = type.substring(startIndex, endIndex); |
| return Class.forName(elementClassName); |
| } |
| |
| private static final class AnnotatedField { |
| private final Field mField; |
| private final Asn1Field mAnnotation; |
| private final Asn1Type mDataType; |
| private final Asn1TagClass mTagClass; |
| private final int mBerTagClass; |
| private final int mBerTagNumber; |
| private final Asn1Tagging mTagging; |
| private final boolean mOptional; |
| |
| public AnnotatedField(Field field, Asn1Field annotation) throws Asn1DecodingException { |
| mField = field; |
| mAnnotation = annotation; |
| mDataType = annotation.type(); |
| |
| Asn1TagClass tagClass = annotation.cls(); |
| if (tagClass == Asn1TagClass.AUTOMATIC) { |
| if (annotation.tagNumber() != -1) { |
| tagClass = Asn1TagClass.CONTEXT_SPECIFIC; |
| } else { |
| tagClass = Asn1TagClass.UNIVERSAL; |
| } |
| } |
| mTagClass = tagClass; |
| mBerTagClass = BerEncoding.getTagClass(mTagClass); |
| |
| int tagNumber; |
| if (annotation.tagNumber() != -1) { |
| tagNumber = annotation.tagNumber(); |
| } else if ((mDataType == Asn1Type.CHOICE) || (mDataType == Asn1Type.ANY)) { |
| tagNumber = -1; |
| } else { |
| tagNumber = BerEncoding.getTagNumber(mDataType); |
| } |
| mBerTagNumber = tagNumber; |
| |
| mTagging = annotation.tagging(); |
| if (((mTagging == Asn1Tagging.EXPLICIT) || (mTagging == Asn1Tagging.IMPLICIT)) |
| && (annotation.tagNumber() == -1)) { |
| throw new Asn1DecodingException( |
| "Tag number must be specified when tagging mode is " + mTagging); |
| } |
| |
| mOptional = annotation.optional(); |
| } |
| |
| public Field getField() { |
| return mField; |
| } |
| |
| public Asn1Field getAnnotation() { |
| return mAnnotation; |
| } |
| |
| public boolean isOptional() { |
| return mOptional; |
| } |
| |
| public int getBerTagClass() { |
| return mBerTagClass; |
| } |
| |
| public int getBerTagNumber() { |
| return mBerTagNumber; |
| } |
| |
| public void setValueFrom(BerDataValue dataValue, Object obj) throws Asn1DecodingException { |
| int readTagClass = dataValue.getTagClass(); |
| if (mBerTagNumber != -1) { |
| int readTagNumber = dataValue.getTagNumber(); |
| if ((readTagClass != mBerTagClass) || (readTagNumber != mBerTagNumber)) { |
| throw new Asn1UnexpectedTagException( |
| "Tag mismatch. Expected: " |
| + BerEncoding.tagClassAndNumberToString(mBerTagClass, mBerTagNumber) |
| + ", but found " |
| + BerEncoding.tagClassAndNumberToString(readTagClass, readTagNumber)); |
| } |
| } else { |
| if (readTagClass != mBerTagClass) { |
| throw new Asn1UnexpectedTagException( |
| "Tag mismatch. Expected class: " |
| + BerEncoding.tagClassToString(mBerTagClass) |
| + ", but found " |
| + BerEncoding.tagClassToString(readTagClass)); |
| } |
| } |
| |
| if (mTagging == Asn1Tagging.EXPLICIT) { |
| try { |
| dataValue = dataValue.contentsReader().readDataValue(); |
| } catch (BerDataValueFormatException e) { |
| throw new Asn1DecodingException( |
| "Failed to read contents of EXPLICIT data value", e); |
| } |
| } |
| |
| BerToJavaConverter.setFieldValue(obj, mField, mDataType, dataValue); |
| } |
| } |
| |
| private static class Asn1UnexpectedTagException extends Asn1DecodingException { |
| private static final long serialVersionUID = 1L; |
| |
| public Asn1UnexpectedTagException(String message) { |
| super(message); |
| } |
| } |
| |
| private static String oidToString(ByteBuffer encodedOid) throws Asn1DecodingException { |
| if (!encodedOid.hasRemaining()) { |
| throw new Asn1DecodingException("Empty OBJECT IDENTIFIER"); |
| } |
| |
| // First component encodes the first two nodes, X.Y, as X * 40 + Y, with 0 <= X <= 2 |
| long firstComponent = decodeBase128UnsignedLong(encodedOid); |
| int firstNode = (int) Math.min(firstComponent / 40, 2); |
| long secondNode = firstComponent - firstNode * 40; |
| StringBuilder result = new StringBuilder(); |
| result.append(Long.toString(firstNode)).append('.') |
| .append(Long.toString(secondNode)); |
| |
| // Each consecutive node is encoded as a separate component |
| while (encodedOid.hasRemaining()) { |
| long node = decodeBase128UnsignedLong(encodedOid); |
| result.append('.').append(Long.toString(node)); |
| } |
| |
| return result.toString(); |
| } |
| |
| private static long decodeBase128UnsignedLong(ByteBuffer encoded) throws Asn1DecodingException { |
| if (!encoded.hasRemaining()) { |
| return 0; |
| } |
| long result = 0; |
| while (encoded.hasRemaining()) { |
| if (result > Long.MAX_VALUE >>> 7) { |
| throw new Asn1DecodingException("Base-128 number too large"); |
| } |
| int b = encoded.get() & 0xff; |
| result <<= 7; |
| result |= b & 0x7f; |
| if ((b & 0x80) == 0) { |
| return result; |
| } |
| } |
| throw new Asn1DecodingException( |
| "Truncated base-128 encoded input: missing terminating byte, with highest bit not" |
| + " set"); |
| } |
| |
| private static BigInteger integerToBigInteger(ByteBuffer encoded) { |
| if (!encoded.hasRemaining()) { |
| return BigInteger.ZERO; |
| } |
| return new BigInteger(ByteBufferUtils.toByteArray(encoded)); |
| } |
| |
| private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException { |
| BigInteger value = integerToBigInteger(encoded); |
| try { |
| return value.intValueExact(); |
| } catch (ArithmeticException e) { |
| throw new Asn1DecodingException( |
| String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value), e); |
| } |
| } |
| |
| private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException { |
| BigInteger value = integerToBigInteger(encoded); |
| try { |
| return value.longValueExact(); |
| } catch (ArithmeticException e) { |
| throw new Asn1DecodingException( |
| String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value), |
| e); |
| } |
| } |
| |
| private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass) |
| throws Asn1DecodingException { |
| Field[] declaredFields = containerClass.getDeclaredFields(); |
| List<AnnotatedField> result = new ArrayList<>(declaredFields.length); |
| for (Field field : declaredFields) { |
| Asn1Field annotation = field.getDeclaredAnnotation(Asn1Field.class); |
| if (annotation == null) { |
| continue; |
| } |
| if (Modifier.isStatic(field.getModifiers())) { |
| throw new Asn1DecodingException( |
| Asn1Field.class.getName() + " used on a static field: " |
| + containerClass.getName() + "." + field.getName()); |
| } |
| |
| AnnotatedField annotatedField; |
| try { |
| annotatedField = new AnnotatedField(field, annotation); |
| } catch (Asn1DecodingException e) { |
| throw new Asn1DecodingException( |
| "Invalid ASN.1 annotation on " |
| + containerClass.getName() + "." + field.getName(), |
| e); |
| } |
| result.add(annotatedField); |
| } |
| return result; |
| } |
| |
| private static final class BerToJavaConverter { |
| private BerToJavaConverter() {} |
| |
| public static void setFieldValue( |
| Object obj, Field field, Asn1Type type, BerDataValue dataValue) |
| throws Asn1DecodingException { |
| try { |
| switch (type) { |
| case SET_OF: |
| case SEQUENCE_OF: |
| if (Asn1OpaqueObject.class.equals(field.getType())) { |
| field.set(obj, convert(type, dataValue, field.getType())); |
| } else { |
| field.set(obj, parseSetOf(dataValue, getElementType(field))); |
| } |
| return; |
| default: |
| field.set(obj, convert(type, dataValue, field.getType())); |
| break; |
| } |
| } catch (ReflectiveOperationException e) { |
| throw new Asn1DecodingException( |
| "Failed to set value of " + obj.getClass().getName() |
| + "." + field.getName(), |
| e); |
| } |
| } |
| |
| private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; |
| |
| @SuppressWarnings("unchecked") |
| public static <T> T convert( |
| Asn1Type sourceType, |
| BerDataValue dataValue, |
| Class<T> targetType) throws Asn1DecodingException { |
| if (ByteBuffer.class.equals(targetType)) { |
| return (T) dataValue.getEncodedContents(); |
| } else if (byte[].class.equals(targetType)) { |
| ByteBuffer resultBuf = dataValue.getEncodedContents(); |
| if (!resultBuf.hasRemaining()) { |
| return (T) EMPTY_BYTE_ARRAY; |
| } |
| byte[] result = new byte[resultBuf.remaining()]; |
| resultBuf.get(result); |
| return (T) result; |
| } else if (Asn1OpaqueObject.class.equals(targetType)) { |
| return (T) new Asn1OpaqueObject(dataValue.getEncoded()); |
| } |
| |
| ByteBuffer encodedContents = dataValue.getEncodedContents(); |
| switch (sourceType) { |
| case INTEGER: |
| if ((int.class.equals(targetType)) || (Integer.class.equals(targetType))) { |
| return (T) Integer.valueOf(integerToInt(encodedContents)); |
| } else if ((long.class.equals(targetType)) || (Long.class.equals(targetType))) { |
| return (T) Long.valueOf(integerToLong(encodedContents)); |
| } else if (BigInteger.class.equals(targetType)) { |
| return (T) integerToBigInteger(encodedContents); |
| } |
| break; |
| case OBJECT_IDENTIFIER: |
| if (String.class.equals(targetType)) { |
| return (T) oidToString(encodedContents); |
| } |
| break; |
| case SEQUENCE: |
| { |
| Asn1Class containerAnnotation = |
| targetType.getDeclaredAnnotation(Asn1Class.class); |
| if ((containerAnnotation != null) |
| && (containerAnnotation.type() == Asn1Type.SEQUENCE)) { |
| return parseSequence(dataValue, targetType); |
| } |
| break; |
| } |
| case CHOICE: |
| { |
| Asn1Class containerAnnotation = |
| targetType.getDeclaredAnnotation(Asn1Class.class); |
| if ((containerAnnotation != null) |
| && (containerAnnotation.type() == Asn1Type.CHOICE)) { |
| return parseChoice(dataValue, targetType); |
| } |
| break; |
| } |
| default: |
| break; |
| } |
| |
| throw new Asn1DecodingException( |
| "Unsupported conversion: ASN.1 " + sourceType + " to " + targetType.getName()); |
| } |
| } |
| } |