From f77821db71423bf8632830d66c0790def700ceee Mon Sep 17 00:00:00 2001 From: Holly Jiuyu Sun Date: Mon, 11 Dec 2017 13:28:03 -0800 Subject: [PATCH] Add asn1 to platform. Add unit test. Bug: 38206971 Test: unit test Change-Id: Ifb7c7d13ad0f980ba986b3aa6c3213ad26658489 --- .../android/internal/telephony/uicc/IccUtils.java | 287 +++++++++- .../internal/telephony/uicc/asn1/Asn1Decoder.java | 151 ++++++ .../internal/telephony/uicc/asn1/Asn1Node.java | 598 +++++++++++++++++++++ .../uicc/asn1/InvalidAsn1DataException.java | 45 ++ .../telephony/uicc/asn1/TagNotFoundException.java | 38 ++ 5 files changed, 1113 insertions(+), 6 deletions(-) create mode 100644 telephony/java/com/android/internal/telephony/uicc/asn1/Asn1Decoder.java create mode 100644 telephony/java/com/android/internal/telephony/uicc/asn1/Asn1Node.java create mode 100644 telephony/java/com/android/internal/telephony/uicc/asn1/InvalidAsn1DataException.java create mode 100644 telephony/java/com/android/internal/telephony/uicc/asn1/TagNotFoundException.java diff --git a/telephony/java/com/android/internal/telephony/uicc/IccUtils.java b/telephony/java/com/android/internal/telephony/uicc/IccUtils.java index 99a82ad00d25..9f8b3a822c81 100644 --- a/telephony/java/com/android/internal/telephony/uicc/IccUtils.java +++ b/telephony/java/com/android/internal/telephony/uicc/IccUtils.java @@ -32,6 +32,12 @@ import java.io.UnsupportedEncodingException; public class IccUtils { static final String LOG_TAG="IccUtils"; + // A table mapping from a number to a hex character for fast encoding hex strings. + private static final char[] HEX_CHARS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + /** * Many fields in GSM SIM's are stored as nibble-swizzled BCD * @@ -62,6 +68,41 @@ public class IccUtils { } /** + * Converts a bcd byte array to String with offset 0 and byte array length. + */ + public static String bcdToString(byte[] data) { + return bcdToString(data, 0, data.length); + } + + /** + * Converts BCD string to bytes. + * + * @param bcd This should have an even length. If not, an "0" will be appended to the string. + */ + public static byte[] bcdToBytes(String bcd) { + byte[] output = new byte[(bcd.length() + 1) / 2]; + bcdToBytes(bcd, output); + return output; + } + + /** + * Converts BCD string to bytes and put it into the given byte array. + * + * @param bcd This should have an even length. If not, an "0" will be appended to the string. + * @param bytes If the array size is less than needed, the rest of the BCD string isn't be + * converted. If the array size is more than needed, the rest of array remains unchanged. + */ + public static void bcdToBytes(String bcd, byte[] bytes) { + if (bcd.length() % 2 != 0) { + bcd += "0"; + } + int size = Math.min(bytes.length * 2, bcd.length()); + for (int i = 0, j = 0; i + 1 < size; i += 2, j++) { + bytes[j] = (byte) (charToByte(bcd.charAt(i + 1)) << 4 | charToByte(bcd.charAt(i))); + } + } + + /** * PLMN (MCC/MNC) is encoded as per 24.008 10.5.1.3 * Returns a concatenated string of MCC+MNC, stripping * all invalid character 'f' @@ -94,10 +135,10 @@ public class IccUtils { int v; v = data[i] & 0xf; - ret.append("0123456789abcdef".charAt(v)); + ret.append(HEX_CHARS[v]); v = (data[i] >> 4) & 0xf; - ret.append("0123456789abcdef".charAt(v)); + ret.append(HEX_CHARS[v]); } return ret.toString(); @@ -305,7 +346,7 @@ public class IccUtils { return GsmAlphabet.gsm8BitUnpackedToString(data, offset, length, defaultCharset.trim()); } - static int + public static int hexCharToInt(char c) { if (c >= '0' && c <= '9') return (c - '0'); if (c >= 'A' && c <= 'F') return (c - 'A' + 10); @@ -361,11 +402,11 @@ public class IccUtils { b = 0x0f & (bytes[i] >> 4); - ret.append("0123456789abcdef".charAt(b)); + ret.append(HEX_CHARS[b]); b = 0x0f & bytes[i]; - ret.append("0123456789abcdef".charAt(b)); + ret.append(HEX_CHARS[b]); } return ret.toString(); @@ -416,7 +457,6 @@ public class IccUtils { if ((data[offset] & 0x40) != 0) { // FIXME(mkf) add country initials here - } return ret; @@ -575,4 +615,239 @@ public class IccUtils { } return iccId.substring( 0, position ); } + + /** + * Converts a series of bytes to an integer. This method currently only supports positive 32-bit + * integers. + * + * @param src The source bytes. + * @param offset The position of the first byte of the data to be converted. The data is base + * 256 with the most significant digit first. + * @param length The length of the data to be converted. It must be <= 4. + * @throws IllegalArgumentException If {@code length} is bigger than 4 or {@code src} cannot be + * parsed as a positive integer. + * @throws IndexOutOfBoundsException If the range defined by {@code offset} and {@code length} + * exceeds the bounds of {@code src}. + */ + public static int bytesToInt(byte[] src, int offset, int length) { + if (length > 4) { + throw new IllegalArgumentException( + "length must be <= 4 (only 32-bit integer supported): " + length); + } + if (offset < 0 || length < 0 || offset + length > src.length) { + throw new IndexOutOfBoundsException( + "Out of the bounds: src=[" + + src.length + + "], offset=" + + offset + + ", length=" + + length); + } + int result = 0; + for (int i = 0; i < length; i++) { + result = (result << 8) | (src[offset + i] & 0xFF); + } + if (result < 0) { + throw new IllegalArgumentException( + "src cannot be parsed as a positive integer: " + result); + } + return result; + } + + /** + * Converts a series of bytes to a raw long variable which can be both positive and negative. + * This method currently only supports 64-bit long variable. + * + * @param src The source bytes. + * @param offset The position of the first byte of the data to be converted. The data is base + * 256 with the most significant digit first. + * @param length The length of the data to be converted. It must be <= 8. + * @throws IllegalArgumentException If {@code length} is bigger than 8. + * @throws IndexOutOfBoundsException If the range defined by {@code offset} and {@code length} + * exceeds the bounds of {@code src}. + */ + public static long bytesToRawLong(byte[] src, int offset, int length) { + if (length > 8) { + throw new IllegalArgumentException( + "length must be <= 8 (only 64-bit long supported): " + length); + } + if (offset < 0 || length < 0 || offset + length > src.length) { + throw new IndexOutOfBoundsException( + "Out of the bounds: src=[" + + src.length + + "], offset=" + + offset + + ", length=" + + length); + } + long result = 0; + for (int i = 0; i < length; i++) { + result = (result << 8) | (src[offset + i] & 0xFF); + } + return result; + } + + /** + * Converts an integer to a new byte array with base 256 and the most significant digit first. + * + * @throws IllegalArgumentException If {@code value} is negative. + */ + public static byte[] unsignedIntToBytes(int value) { + if (value < 0) { + throw new IllegalArgumentException("value must be 0 or positive: " + value); + } + byte[] bytes = new byte[byteNumForUnsignedInt(value)]; + unsignedIntToBytes(value, bytes, 0); + return bytes; + } + + /** + * Converts an integer to a new byte array with base 256 and the most significant digit first. + * The first byte's highest bit is used for sign. If the most significant digit is larger than + * 127, an extra byte (0) will be prepended before it. This method currently doesn't support + * negative values. + * + * @throws IllegalArgumentException If {@code value} is negative. + */ + public static byte[] signedIntToBytes(int value) { + if (value < 0) { + throw new IllegalArgumentException("value must be 0 or positive: " + value); + } + byte[] bytes = new byte[byteNumForSignedInt(value)]; + signedIntToBytes(value, bytes, 0); + return bytes; + } + + /** + * Converts an integer to a series of bytes with base 256 and the most significant digit first. + * + * @param value The integer to be converted. + * @param dest The destination byte array. + * @param offset The start offset of the byte array. + * @return The number of byte needeed. + * @throws IllegalArgumentException If {@code value} is negative. + * @throws IndexOutOfBoundsException If {@code offset} exceeds the bounds of {@code dest}. + */ + public static int unsignedIntToBytes(int value, byte[] dest, int offset) { + return intToBytes(value, dest, offset, false); + } + + /** + * Converts an integer to a series of bytes with base 256 and the most significant digit first. + * The first byte's highest bit is used for sign. If the most significant digit is larger than + * 127, an extra byte (0) will be prepended before it. This method currently doesn't support + * negative values. + * + * @throws IllegalArgumentException If {@code value} is negative. + * @throws IndexOutOfBoundsException If {@code offset} exceeds the bounds of {@code dest}. + */ + public static int signedIntToBytes(int value, byte[] dest, int offset) { + return intToBytes(value, dest, offset, true); + } + + /** + * Calculates the number of required bytes to represent {@code value}. The bytes will be base + * 256 with the most significant digit first. + * + * @throws IllegalArgumentException If {@code value} is negative. + */ + public static int byteNumForUnsignedInt(int value) { + return byteNumForInt(value, false); + } + + /** + * Calculates the number of required bytes to represent {@code value}. The bytes will be base + * 256 with the most significant digit first. If the most significant digit is larger than 127, + * an extra byte (0) will be prepended before it. This method currently only supports positive + * integers. + * + * @throws IllegalArgumentException If {@code value} is negative. + */ + public static int byteNumForSignedInt(int value) { + return byteNumForInt(value, true); + } + + private static int intToBytes(int value, byte[] dest, int offset, boolean signed) { + int l = byteNumForInt(value, signed); + if (offset < 0 || offset + l > dest.length) { + throw new IndexOutOfBoundsException("Not enough space to write. Required bytes: " + l); + } + for (int i = l - 1, v = value; i >= 0; i--, v >>>= 8) { + byte b = (byte) (v & 0xFF); + dest[offset + i] = b; + } + return l; + } + + private static int byteNumForInt(int value, boolean signed) { + if (value < 0) { + throw new IllegalArgumentException("value must be 0 or positive: " + value); + } + if (signed) { + if (value <= 0x7F) { + return 1; + } + if (value <= 0x7FFF) { + return 2; + } + if (value <= 0x7FFFFF) { + return 3; + } + } else { + if (value <= 0xFF) { + return 1; + } + if (value <= 0xFFFF) { + return 2; + } + if (value <= 0xFFFFFF) { + return 3; + } + } + return 4; + } + + + /** + * Counts the number of trailing zero bits of a byte. + */ + public static byte countTrailingZeros(byte b) { + if (b == 0) { + return 8; + } + int v = b & 0xFF; + byte c = 7; + if ((v & 0x0F) != 0) { + c -= 4; + } + if ((v & 0x33) != 0) { + c -= 2; + } + if ((v & 0x55) != 0) { + c -= 1; + } + return c; + } + + /** + * Converts a byte to a hex string. + */ + public static String byteToHex(byte b) { + return new String(new char[] {HEX_CHARS[(b & 0xFF) >>> 4], HEX_CHARS[b & 0xF]}); + } + + /** + * Converts a character of [0-9a-aA-F] to its hex value in a byte. If the character is not a + * hex number, 0 will be returned. + */ + private static byte charToByte(char c) { + if (c >= 0x30 && c <= 0x39) { + return (byte) (c - 0x30); + } else if (c >= 0x41 && c <= 0x46) { + return (byte) (c - 0x37); + } else if (c >= 0x61 && c <= 0x66) { + return (byte) (c - 0x57); + } + return 0; + } } diff --git a/telephony/java/com/android/internal/telephony/uicc/asn1/Asn1Decoder.java b/telephony/java/com/android/internal/telephony/uicc/asn1/Asn1Decoder.java new file mode 100644 index 000000000000..1ad0b664ed4e --- /dev/null +++ b/telephony/java/com/android/internal/telephony/uicc/asn1/Asn1Decoder.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2016 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.internal.telephony.uicc.asn1; + +import com.android.internal.telephony.uicc.IccUtils; + +/** + * This represents a decoder helping decode an array of bytes or a hex string into + * {@link Asn1Node}s. This class tracks the next position for decoding. This class is not + * thread-safe. + */ +public final class Asn1Decoder { + // Source byte array. + private final byte[] mSrc; + // Next position of the byte in the source array for decoding. + private int mPosition; + // Exclusive end of the range in the array for decoding. + private final int mEnd; + + /** Creates a decoder on a hex string. */ + public Asn1Decoder(String hex) { + this(IccUtils.hexStringToBytes(hex)); + } + + /** Creates a decoder on a byte array. */ + public Asn1Decoder(byte[] src) { + this(src, 0, src.length); + } + + /** + * Creates a decoder on a byte array slice. + * + * @throws IndexOutOfBoundsException If the range defined by {@code offset} and {@code length} + * exceeds the bounds of {@code bytes}. + */ + public Asn1Decoder(byte[] bytes, int offset, int length) { + if (offset < 0 || length < 0 || offset + length > bytes.length) { + throw new IndexOutOfBoundsException( + "Out of the bounds: bytes=[" + + bytes.length + + "], offset=" + + offset + + ", length=" + + length); + } + mSrc = bytes; + mPosition = offset; + mEnd = offset + length; + } + + /** @return The next start position for decoding. */ + public int getPosition() { + return mPosition; + } + + /** Returns whether the node has a next node. */ + public boolean hasNextNode() { + return mPosition < mEnd; + } + + /** + * Parses the next node. If the node is a constructed node, its children will be parsed only + * when they are accessed, e.g., though {@link Asn1Node#getChildren()}. + * + * @return The next decoded {@link Asn1Node}. If success, the next decoding position will also + * be updated. If any error happens, e.g., moving over the end position, {@code null} + * will be returned and the next decoding position won't be modified. + * @throws InvalidAsn1DataException If the bytes cannot be parsed. + */ + public Asn1Node nextNode() throws InvalidAsn1DataException { + if (mPosition >= mEnd) { + throw new IllegalStateException("No bytes to parse."); + } + + int offset = mPosition; + + // Extracts the tag. + int tagStart = offset; + byte b = mSrc[offset++]; + if ((b & 0x1F) == 0x1F) { + // High-tag-number form + while (offset < mEnd && (mSrc[offset++] & 0x80) != 0) { + // Do nothing. + } + } + if (offset >= mEnd) { + // No length bytes or the tag is too long. + throw new InvalidAsn1DataException(0, "Invalid length at position: " + offset); + } + int tag; + try { + tag = IccUtils.bytesToInt(mSrc, tagStart, offset - tagStart); + } catch (IllegalArgumentException e) { + // Cannot parse the tag as an integer. + throw new InvalidAsn1DataException(0, "Cannot parse tag at position: " + tagStart, e); + } + + // Extracts the length. + int dataLen; + b = mSrc[offset++]; + if ((b & 0x80) == 0) { + // Short-form length + dataLen = b; + } else { + // Long-form length + int lenLen = b & 0x7F; + if (offset + lenLen > mEnd) { + // No enough bytes for the long-form length + throw new InvalidAsn1DataException( + tag, "Cannot parse length at position: " + offset); + } + try { + dataLen = IccUtils.bytesToInt(mSrc, offset, lenLen); + } catch (IllegalArgumentException e) { + // Cannot parse the data length as an integer. + throw new InvalidAsn1DataException( + tag, "Cannot parse length at position: " + offset, e); + } + offset += lenLen; + } + if (offset + dataLen > mEnd) { + // No enough data left. + throw new InvalidAsn1DataException( + tag, + "Incomplete data at position: " + + offset + + ", expected bytes: " + + dataLen + + ", actual bytes: " + + (mEnd - offset)); + } + + Asn1Node root = new Asn1Node(tag, mSrc, offset, dataLen); + mPosition = offset + dataLen; + return root; + } +} diff --git a/telephony/java/com/android/internal/telephony/uicc/asn1/Asn1Node.java b/telephony/java/com/android/internal/telephony/uicc/asn1/Asn1Node.java new file mode 100644 index 000000000000..5eb1d5c5936a --- /dev/null +++ b/telephony/java/com/android/internal/telephony/uicc/asn1/Asn1Node.java @@ -0,0 +1,598 @@ +/* + * Copyright (C) 2016 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.internal.telephony.uicc.asn1; + +import android.annotation.Nullable; + +import com.android.internal.telephony.uicc.IccUtils; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * This represents a primitive or constructed data defined by ASN.1. A constructed node can have + * child nodes. A non-constructed node can have a value. This class is read-only. To build a node, + * you can use the {@link #newBuilder(int)} method to get a {@link Builder} instance. This class is + * not thread-safe. + */ +public final class Asn1Node { + private static final int INT_BYTES = Integer.SIZE / Byte.SIZE; + private static final List EMPTY_NODE_LIST = Collections.emptyList(); + + // Bytes for boolean values. + private static final byte[] TRUE_BYTES = new byte[] {-1}; + private static final byte[] FALSE_BYTES = new byte[] {0}; + + /** + * This class is used to build an Asn1Node instance of a constructed tag. This class is not + * thread-safe. + */ + public static final class Builder { + private final int mTag; + private final List mChildren; + + private Builder(int tag) { + if (!isConstructedTag(tag)) { + throw new IllegalArgumentException( + "Builder should be created for a constructed tag: " + tag); + } + mTag = tag; + mChildren = new ArrayList<>(); + } + + /** + * Adds a child from an existing node. + * + * @return This builder. + * @throws IllegalArgumentException If the child is a non-existing node. + */ + public Builder addChild(Asn1Node child) { + mChildren.add(child); + return this; + } + + /** + * Adds a child from another builder. The child will be built with the call to this method, + * and any changes to the child builder after the call to this method doesn't have effect. + * + * @return This builder. + */ + public Builder addChild(Builder child) { + mChildren.add(child.build()); + return this; + } + + /** + * Adds children from bytes. This method calls {@link Asn1Decoder} to verify the {@code + * encodedBytes} and adds all nodes parsed from it as children. + * + * @return This builder. + * @throws InvalidAsn1DataException If the data bytes cannot be parsed. + */ + public Builder addChildren(byte[] encodedBytes) throws InvalidAsn1DataException { + Asn1Decoder subDecoder = new Asn1Decoder(encodedBytes, 0, encodedBytes.length); + while (subDecoder.hasNextNode()) { + mChildren.add(subDecoder.nextNode()); + } + return this; + } + + /** + * Adds a child of non-constructed tag with an integer as the data. + * + * @return This builder. + * @throws IllegalStateException If the {@code tag} is not constructed.. + */ + public Builder addChildAsInteger(int tag, int value) { + if (isConstructedTag(tag)) { + throw new IllegalStateException("Cannot set value of a constructed tag: " + tag); + } + byte[] dataBytes = IccUtils.signedIntToBytes(value); + addChild(new Asn1Node(tag, dataBytes, 0, dataBytes.length)); + return this; + } + + /** + * Adds a child of non-constructed tag with a string as the data. + * + * @return This builder. + * @throws IllegalStateException If the {@code tag} is not constructed.. + */ + public Builder addChildAsString(int tag, String value) { + if (isConstructedTag(tag)) { + throw new IllegalStateException("Cannot set value of a constructed tag: " + tag); + } + byte[] dataBytes = value.getBytes(StandardCharsets.UTF_8); + addChild(new Asn1Node(tag, dataBytes, 0, dataBytes.length)); + return this; + } + + /** + * Adds a child of non-constructed tag with a byte array as the data. + * + * @param value The value will be owned by this node. + * @return This builder. + * @throws IllegalStateException If the {@code tag} is not constructed.. + */ + public Builder addChildAsBytes(int tag, byte[] value) { + if (isConstructedTag(tag)) { + throw new IllegalStateException("Cannot set value of a constructed tag: " + tag); + } + addChild(new Asn1Node(tag, value, 0, value.length)); + return this; + } + + /** + * Adds a child of non-constructed tag with a byte array as the data from a hex string. + * + * @return This builder. + * @throws IllegalStateException If the {@code tag} is not constructed.. + */ + public Builder addChildAsBytesFromHex(int tag, String hex) { + return addChildAsBytes(tag, IccUtils.hexStringToBytes(hex)); + } + + /** + * Adds a child of non-constructed tag with bits as the data. + * + * @return This builder. + * @throws IllegalStateException If the {@code tag} is not constructed.. + */ + public Builder addChildAsBits(int tag, int value) { + if (isConstructedTag(tag)) { + throw new IllegalStateException("Cannot set value of a constructed tag: " + tag); + } + // Always allocate 5 bytes for simplicity. + byte[] dataBytes = new byte[INT_BYTES + 1]; + // Puts the integer into the byte[1-4]. + value = Integer.reverse(value); + int dataLength = 0; + for (int i = 1; i < dataBytes.length; i++) { + dataBytes[i] = (byte) (value >> ((INT_BYTES - i) * Byte.SIZE)); + if (dataBytes[i] != 0) { + dataLength = i; + } + } + dataLength++; + // The first byte is the number of trailing zeros of the last byte. + dataBytes[0] = IccUtils.countTrailingZeros(dataBytes[dataLength - 1]); + addChild(new Asn1Node(tag, dataBytes, 0, dataLength)); + return this; + } + + /** + * Adds a child of non-constructed tag with a boolean as the data. + * + * @return This builder. + * @throws IllegalStateException If the {@code tag} is not constructed.. + */ + public Builder addChildAsBoolean(int tag, boolean value) { + if (isConstructedTag(tag)) { + throw new IllegalStateException("Cannot set value of a constructed tag: " + tag); + } + addChild(new Asn1Node(tag, value ? TRUE_BYTES : FALSE_BYTES, 0, 1)); + return this; + } + + /** Builds the node. */ + public Asn1Node build() { + return new Asn1Node(mTag, mChildren); + } + } + + private final int mTag; + private final boolean mConstructed; + // Do not use this field directly in the methods other than the constructor and encoding + // methods (e.g., toBytes()), but always use getChildren() instead. + private final List mChildren; + + // Byte array that actually holds the data. For a non-constructed node, this stores its actual + // value. If the value is not set, this is null. For constructed node, this stores encoded data + // of its children, which will be decoded on the first call to getChildren(). + private @Nullable byte[] mDataBytes; + // Offset of the data in above byte array. + private int mDataOffset; + // Length of the data in above byte array. If it's a constructed node, this is always the total + // length of all its children. + private int mDataLength; + // Length of the total bytes required to encode this node. + private int mEncodedLength; + + /** + * Creates a new ASN.1 data node builder with the given tag. The tag is an encoded tag including + * the tag class, tag number, and constructed mask. + */ + public static Builder newBuilder(int tag) { + return new Builder(tag); + } + + private static boolean isConstructedTag(int tag) { + // Constructed mask is at the 6th bit. + byte[] tagBytes = IccUtils.unsignedIntToBytes(tag); + return (tagBytes[0] & 0x20) != 0; + } + + private static int calculateEncodedBytesNumForLength(int length) { + // Constructed mask is at the 6th bit. + int len = 1; + if (length > 127) { + len += IccUtils.byteNumForUnsignedInt(length); + } + return len; + } + + /** + * Creates a node with given data bytes. If it is a constructed node, its children will be + * parsed when they are visited. + */ + Asn1Node(int tag, @Nullable byte[] src, int offset, int length) { + mTag = tag; + // Constructed mask is at the 6th bit. + mConstructed = isConstructedTag(tag); + mDataBytes = src; + mDataOffset = offset; + mDataLength = length; + mChildren = mConstructed ? new ArrayList() : EMPTY_NODE_LIST; + mEncodedLength = + IccUtils.byteNumForUnsignedInt(mTag) + + calculateEncodedBytesNumForLength(mDataLength) + + mDataLength; + } + + /** Creates a constructed node with given children. */ + private Asn1Node(int tag, List children) { + mTag = tag; + mConstructed = true; + mChildren = children; + + mDataLength = 0; + int size = children.size(); + for (int i = 0; i < size; i++) { + mDataLength += children.get(i).mEncodedLength; + } + mEncodedLength = + IccUtils.byteNumForUnsignedInt(mTag) + + calculateEncodedBytesNumForLength(mDataLength) + + mDataLength; + } + + public int getTag() { + return mTag; + } + + public boolean isConstructed() { + return mConstructed; + } + + /** + * Tests if a node has a child. + * + * @param tag The tag of an immediate child. + * @param tags The tags of lineal descendant. + */ + public boolean hasChild(int tag, int... tags) throws InvalidAsn1DataException { + try { + getChild(tag, tags); + } catch (TagNotFoundException e) { + return false; + } + return true; + } + + /** + * Gets the first child node having the given {@code tag} and {@code tags}. + * + * @param tag The tag of an immediate child. + * @param tags The tags of lineal descendant. + * @throws TagNotFoundException If the child cannot be found. + */ + public Asn1Node getChild(int tag, int... tags) + throws TagNotFoundException, InvalidAsn1DataException { + if (!mConstructed) { + throw new TagNotFoundException(tag); + } + int index = 0; + Asn1Node node = this; + while (node != null) { + List children = node.getChildren(); + int size = children.size(); + Asn1Node foundChild = null; + for (int i = 0; i < size; i++) { + Asn1Node child = children.get(i); + if (child.getTag() == tag) { + foundChild = child; + break; + } + } + node = foundChild; + if (index >= tags.length) { + break; + } + tag = tags[index++]; + } + if (node == null) { + throw new TagNotFoundException(tag); + } + return node; + } + + /** + * Gets all child nodes which have the given {@code tag}. + * + * @return If this is primitive or no such children are found, an empty list will be returned. + */ + public List getChildren(int tag) + throws TagNotFoundException, InvalidAsn1DataException { + if (!mConstructed) { + return EMPTY_NODE_LIST; + } + + List children = getChildren(); + if (children.isEmpty()) { + return EMPTY_NODE_LIST; + } + List output = new ArrayList<>(); + int size = children.size(); + for (int i = 0; i < size; i++) { + Asn1Node child = children.get(i); + if (child.getTag() == tag) { + output.add(child); + } + } + return output.isEmpty() ? EMPTY_NODE_LIST : output; + } + + /** + * Gets all child nodes of this node. If it's a constructed node having encoded data, it's + * children will be decoded here. + * + * @return If this is primitive, an empty list will be returned. Do not modify the returned list + * directly. + */ + public List getChildren() throws InvalidAsn1DataException { + if (!mConstructed) { + return EMPTY_NODE_LIST; + } + + if (mDataBytes != null) { + Asn1Decoder subDecoder = new Asn1Decoder(mDataBytes, mDataOffset, mDataLength); + while (subDecoder.hasNextNode()) { + mChildren.add(subDecoder.nextNode()); + } + mDataBytes = null; + mDataOffset = 0; + } + return mChildren; + } + + /** @return Whether this node has a value. False will be returned for a constructed node. */ + public boolean hasValue() { + return !mConstructed && mDataBytes != null; + } + + /** + * @return The data as an integer. If the data length is larger than 4, only the first 4 bytes + * will be parsed. + * @throws InvalidAsn1DataException If the data bytes cannot be parsed. + */ + public int asInteger() throws InvalidAsn1DataException { + if (mConstructed) { + throw new IllegalStateException("Cannot get value of a constructed node."); + } + if (mDataBytes == null) { + throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null."); + } + try { + return IccUtils.bytesToInt(mDataBytes, mDataOffset, mDataLength); + } catch (IllegalArgumentException | IndexOutOfBoundsException e) { + throw new InvalidAsn1DataException(mTag, "Cannot parse data bytes.", e); + } + } + + /** + * @return The data as a long variable which can be both positive and negative. If the data + * length is larger than 8, only the first 8 bytes will be parsed. + * @throws InvalidAsn1DataException If the data bytes cannot be parsed. + */ + public long asRawLong() throws InvalidAsn1DataException { + if (mConstructed) { + throw new IllegalStateException("Cannot get value of a constructed node."); + } + if (mDataBytes == null) { + throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null."); + } + try { + return IccUtils.bytesToRawLong(mDataBytes, mDataOffset, mDataLength); + } catch (IllegalArgumentException | IndexOutOfBoundsException e) { + throw new InvalidAsn1DataException(mTag, "Cannot parse data bytes.", e); + } + } + + /** + * @return The data as a string in UTF-8 encoding. + * @throws InvalidAsn1DataException If the data bytes cannot be parsed. + */ + public String asString() throws InvalidAsn1DataException { + if (mConstructed) { + throw new IllegalStateException("Cannot get value of a constructed node."); + } + if (mDataBytes == null) { + throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null."); + } + try { + return new String(mDataBytes, mDataOffset, mDataLength, StandardCharsets.UTF_8); + } catch (IndexOutOfBoundsException e) { + throw new InvalidAsn1DataException(mTag, "Cannot parse data bytes.", e); + } + } + + /** + * @return The data as a byte array. + * @throws InvalidAsn1DataException If the data bytes cannot be parsed. + */ + public byte[] asBytes() throws InvalidAsn1DataException { + if (mConstructed) { + throw new IllegalStateException("Cannot get value of a constructed node."); + } + if (mDataBytes == null) { + throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null."); + } + byte[] output = new byte[mDataLength]; + try { + System.arraycopy(mDataBytes, mDataOffset, output, 0, mDataLength); + } catch (IndexOutOfBoundsException e) { + throw new InvalidAsn1DataException(mTag, "Cannot parse data bytes.", e); + } + return output; + } + + /** + * Gets the data as an integer for BIT STRING. DER actually stores the bits in a reversed order. + * The returned integer here has the order fixed (first bit is at the lowest position). This + * method currently only support at most 32 bits which fit in an integer. + * + * @return The data as an integer. If this is constructed, a {@code null} will be returned. + * @throws InvalidAsn1DataException If the data bytes cannot be parsed. + */ + public int asBits() throws InvalidAsn1DataException { + if (mConstructed) { + throw new IllegalStateException("Cannot get value of a constructed node."); + } + if (mDataBytes == null) { + throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null."); + } + int bits; + try { + bits = IccUtils.bytesToInt(mDataBytes, mDataOffset + 1, mDataLength - 1); + } catch (IllegalArgumentException | IndexOutOfBoundsException e) { + throw new InvalidAsn1DataException(mTag, "Cannot parse data bytes.", e); + } + for (int i = mDataLength - 1; i < INT_BYTES; i++) { + bits <<= Byte.SIZE; + } + return Integer.reverse(bits); + } + + /** + * @return The data as a boolean. + * @throws InvalidAsn1DataException If the data bytes cannot be parsed. + */ + public boolean asBoolean() throws InvalidAsn1DataException { + if (mConstructed) { + throw new IllegalStateException("Cannot get value of a constructed node."); + } + if (mDataBytes == null) { + throw new InvalidAsn1DataException(mTag, "Data bytes cannot be null."); + } + if (mDataLength != 1) { + throw new InvalidAsn1DataException( + mTag, "Cannot parse data bytes as boolean: length=" + mDataLength); + } + if (mDataOffset < 0 || mDataOffset >= mDataBytes.length) { + throw new InvalidAsn1DataException( + mTag, + "Cannot parse data bytes.", + new ArrayIndexOutOfBoundsException(mDataOffset)); + } + // ASN.1 has "true" as 0xFF. + if (mDataBytes[mDataOffset] == -1) { + return Boolean.TRUE; + } else if (mDataBytes[mDataOffset] == 0) { + return Boolean.FALSE; + } + throw new InvalidAsn1DataException( + mTag, "Cannot parse data bytes as boolean: " + mDataBytes[mDataOffset]); + } + + /** @return The number of required bytes for encoding this node in DER. */ + public int getEncodedLength() { + return mEncodedLength; + } + + /** @return The number of required bytes for encoding this node's data in DER. */ + public int getDataLength() { + return mDataLength; + } + + /** + * Writes the DER encoded bytes of this node into a byte array. The number of written bytes is + * {@link #getEncodedLength()}. + * + * @throws IndexOutOfBoundsException If the {@code dest} doesn't have enough space to write. + */ + public void writeToBytes(byte[] dest, int offset) { + if (offset < 0 || offset + mEncodedLength > dest.length) { + throw new IndexOutOfBoundsException( + "Not enough space to write. Required bytes: " + mEncodedLength); + } + write(dest, offset); + } + + /** Writes the DER encoded bytes of this node into a new byte array. */ + public byte[] toBytes() { + byte[] dest = new byte[mEncodedLength]; + write(dest, 0); + return dest; + } + + /** Gets a hex string representing the DER encoded bytes of this node. */ + public String toHex() { + return IccUtils.bytesToHexString(toBytes()); + } + + /** Gets header (tag + length) as hex string. */ + public String getHeadAsHex() { + String headHex = IccUtils.bytesToHexString(IccUtils.unsignedIntToBytes(mTag)); + if (mDataLength <= 127) { + headHex += IccUtils.byteToHex((byte) mDataLength); + } else { + byte[] lenBytes = IccUtils.unsignedIntToBytes(mDataLength); + headHex += IccUtils.byteToHex((byte) (lenBytes.length | 0x80)); + headHex += IccUtils.bytesToHexString(lenBytes); + } + return headHex; + } + + /** Returns the new offset where to write the next node data. */ + private int write(byte[] dest, int offset) { + // Writes the tag. + offset += IccUtils.unsignedIntToBytes(mTag, dest, offset); + // Writes the length. + if (mDataLength <= 127) { + dest[offset++] = (byte) mDataLength; + } else { + // Bytes required for encoding the length + int lenLen = IccUtils.unsignedIntToBytes(mDataLength, dest, ++offset); + dest[offset - 1] = (byte) (lenLen | 0x80); + offset += lenLen; + } + // Writes the data. + if (mConstructed && mDataBytes == null) { + int size = mChildren.size(); + for (int i = 0; i < size; i++) { + Asn1Node child = mChildren.get(i); + offset = child.write(dest, offset); + } + } else if (mDataBytes != null) { + System.arraycopy(mDataBytes, mDataOffset, dest, offset, mDataLength); + offset += mDataLength; + } + return offset; + } +} diff --git a/telephony/java/com/android/internal/telephony/uicc/asn1/InvalidAsn1DataException.java b/telephony/java/com/android/internal/telephony/uicc/asn1/InvalidAsn1DataException.java new file mode 100644 index 000000000000..c151468b34a7 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/uicc/asn1/InvalidAsn1DataException.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2016 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.internal.telephony.uicc.asn1; + +/** + * Exception for invalid ASN.1 data in DER encoding which cannot be parsed as a node or a specific + * data type. + */ +public class InvalidAsn1DataException extends Exception { + private final int mTag; + + public InvalidAsn1DataException(int tag, String message) { + super(message); + mTag = tag; + } + + public InvalidAsn1DataException(int tag, String message, Throwable throwable) { + super(message, throwable); + mTag = tag; + } + + /** @return The tag which has the invalid data. */ + public int getTag() { + return mTag; + } + + @Override + public String getMessage() { + return super.getMessage() + " (tag=" + mTag + ")"; + } +} diff --git a/telephony/java/com/android/internal/telephony/uicc/asn1/TagNotFoundException.java b/telephony/java/com/android/internal/telephony/uicc/asn1/TagNotFoundException.java new file mode 100644 index 000000000000..f79021eb6106 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/uicc/asn1/TagNotFoundException.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016 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.internal.telephony.uicc.asn1; + +/** + * Exception for getting a child of a {@link Asn1Node} with a non-existing tag. + */ +public class TagNotFoundException extends Exception { + private final int mTag; + + public TagNotFoundException(int tag) { + mTag = tag; + } + + /** @return The tag which has the invalid data. */ + public int getTag() { + return mTag; + } + + @Override + public String getMessage() { + return super.getMessage() + " (tag=" + mTag + ")"; + } +} -- 2.11.0