blob: 5eb1d5c5936ac308c990a70cd28d26092bb5f37b [file] [log] [blame]
/*
* 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<Asn1Node> 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<Asn1Node> 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<Asn1Node> 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<Asn1Node>() : EMPTY_NODE_LIST;
mEncodedLength =
IccUtils.byteNumForUnsignedInt(mTag)
+ calculateEncodedBytesNumForLength(mDataLength)
+ mDataLength;
}
/** Creates a constructed node with given children. */
private Asn1Node(int tag, List<Asn1Node> 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<Asn1Node> 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<Asn1Node> getChildren(int tag)
throws TagNotFoundException, InvalidAsn1DataException {
if (!mConstructed) {
return EMPTY_NODE_LIST;
}
List<Asn1Node> children = getChildren();
if (children.isEmpty()) {
return EMPTY_NODE_LIST;
}
List<Asn1Node> 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<Asn1Node> 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;
}
}