blob: 6fd7c68b6d1c5f8495577cb0c7236e0c2333e0c9 [file] [log] [blame]
/*
* Copyright 2018 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;
import android.annotation.Nullable;
import android.telephony.Rlog;
import android.util.ArrayMap;
import com.android.internal.annotations.VisibleForTesting;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* This class parses an Answer To Reset (ATR) message.
* The ATR message structure is defined in standard ISO/IEC 7816-3. The eUICC related ATR message
* is defined in standard ETSI TS 102 221 V14.0.0.
*/
public class AnswerToReset {
private static final String TAG = "AnswerToReset";
private static final boolean VDBG = false; // STOPSHIP if true
private static final int TAG_CARD_CAPABILITIES = 0x07;
private static final int EXTENDED_APDU_INDEX = 2;
private static final int B8_MASK = 0x80;
private static final int B7_MASK = 0x40;
private static final int B2_MASK = 0x02;
public static final byte DIRECT_CONVENTION = (byte) 0x3B;
public static final byte INVERSE_CONVENTION = (byte) 0x3F;
public static final int INTERFACE_BYTES_MASK = 0xF0;
public static final int T_MASK = 0x0F;
public static final int T_VALUE_FOR_GLOBAL_INTERFACE = 15;
public static final int TA_MASK = 0x10;
public static final int TB_MASK = 0x20;
public static final int TC_MASK = 0x40;
public static final int TD_MASK = 0x80;
private boolean mIsDirectConvention;
private boolean mOnlyTEqualsZero = true;
private boolean mIsEuiccSupported;
private byte mFormatByte;
private ArrayList<InterfaceByte> mInterfaceBytes = new ArrayList<>();
private HistoricalBytes mHistoricalBytes;
private Byte mCheckByte;
/** Class for the historical bytes. */
public static class HistoricalBytes {
private static final int TAG_MASK = 0xF0;
private static final int LENGTH_MASK = 0x0F;
private final byte[] mRawData;
private final ArrayMap<Integer, byte[]> mNodes;
private final byte mCategory;
/** Get the category of the historical bytes. */
public byte getCategory() {
return mCategory;
}
/** Get the raw data of historical bytes. */
public byte[] getRawData() {
return mRawData;
}
/** Get the value of the tag in historical bytes. */
@Nullable
public byte[] getValue(int tag) {
return mNodes.get(tag);
}
@Nullable
private static HistoricalBytes parseHistoricalBytes(
byte[] originalData, int startIndex, int length) {
if (length <= 0 || startIndex + length > originalData.length) {
return null;
}
ArrayMap<Integer, byte[]> nodes = new ArrayMap<>();
// Start parsing from second byte since the first one is category.
int index = startIndex + 1;
while (index < startIndex + length && index > 0) {
index = parseLtvNode(index, nodes, originalData, startIndex + length - 1);
}
if (index < 0) {
return null;
}
byte[] rawData = new byte[length];
System.arraycopy(originalData, startIndex, rawData, 0, length);
return new HistoricalBytes(rawData, nodes, rawData[0]);
}
private HistoricalBytes(byte[] rawData, ArrayMap<Integer, byte[]> nodes, byte category) {
mRawData = rawData;
mNodes = nodes;
mCategory = category;
}
private static int parseLtvNode(
int index, ArrayMap<Integer, byte[]> nodes, byte[] data, int lastByteIndex) {
if (index > lastByteIndex) {
return -1;
}
int tag = (data[index] & TAG_MASK) >> 4;
int length = data[index++] & LENGTH_MASK;
if (index + length > lastByteIndex + 1 || length == 0) {
return -1;
}
byte[] value = new byte[length];
System.arraycopy(data, index, value, 0, length);
nodes.put(tag, value);
return index + length;
}
}
/**
* Returns an AnswerToReset by parsing the input atr string, return null if the parsing fails.
*/
public static AnswerToReset parseAtr(String atr) {
AnswerToReset answerToReset = new AnswerToReset();
if (answerToReset.parseAtrString(atr)) {
return answerToReset;
}
return null;
}
private AnswerToReset() {}
private static String byteToStringHex(Byte b) {
return b == null ? null : IccUtils.byteToHex(b);
}
private void checkIsEuiccSupported() {
// eUICC is supported only if the b8 and b2 of the first tB after T=15 are set to 1.
for (int i = 0; i < mInterfaceBytes.size() - 1; i++) {
if (mInterfaceBytes.get(i).getTD() != null
&& (mInterfaceBytes.get(i).getTD() & T_MASK) == T_VALUE_FOR_GLOBAL_INTERFACE
&& mInterfaceBytes.get(i + 1).getTB() != null
&& (mInterfaceBytes.get(i + 1).getTB() & B8_MASK) != 0
&& (mInterfaceBytes.get(i + 1).getTB() & B2_MASK) != 0) {
mIsEuiccSupported = true;
return;
}
}
}
private int parseConventionByte(byte[] atrBytes, int index) {
if (index >= atrBytes.length) {
loge("Failed to read the convention byte.");
return -1;
}
byte value = atrBytes[index];
if (value == DIRECT_CONVENTION) {
mIsDirectConvention = true;
} else if (value == INVERSE_CONVENTION) {
mIsDirectConvention = false;
} else {
loge("Unrecognized convention byte " + IccUtils.byteToHex(value));
return -1;
}
return index + 1;
}
private int parseFormatByte(byte[] atrBytes, int index) {
if (index >= atrBytes.length) {
loge("Failed to read the format byte.");
return -1;
}
mFormatByte = atrBytes[index];
if (VDBG) log("mHistoricalBytesLength: " + (mFormatByte & T_MASK));
return index + 1;
}
private int parseInterfaceBytes(byte[] atrBytes, int index) {
// The first lastTD is actually not any TD but instead the format byte.
byte lastTD = mFormatByte;
while (true) {
if (VDBG) log("lastTD: " + IccUtils.byteToHex(lastTD));
// Parse the interface bytes.
if ((lastTD & INTERFACE_BYTES_MASK) == 0) {
break;
}
InterfaceByte interfaceByte = new InterfaceByte();
if (VDBG) log("lastTD & TA_MASK: " + IccUtils.byteToHex((byte) (lastTD & TA_MASK)));
if ((lastTD & TA_MASK) != 0) {
if (index >= atrBytes.length) {
loge("Failed to read the byte for TA.");
return -1;
}
interfaceByte.setTA(atrBytes[index]);
index++;
}
if (VDBG) log("lastTD & TB_MASK: " + IccUtils.byteToHex((byte) (lastTD & TB_MASK)));
if ((lastTD & TB_MASK) != 0) {
if (index >= atrBytes.length) {
loge("Failed to read the byte for TB.");
return -1;
}
interfaceByte.setTB(atrBytes[index]);
index++;
}
if (VDBG) log("lastTD & TC_MASK: " + IccUtils.byteToHex((byte) (lastTD & TC_MASK)));
if ((lastTD & TC_MASK) != 0) {
if (index >= atrBytes.length) {
loge("Failed to read the byte for TC.");
return -1;
}
interfaceByte.setTC(atrBytes[index]);
index++;
}
if (VDBG) log("lastTD & TD_MASK: " + IccUtils.byteToHex((byte) (lastTD & TD_MASK)));
if ((lastTD & TD_MASK) != 0) {
if (index >= atrBytes.length) {
loge("Failed to read the byte for TD.");
return -1;
}
interfaceByte.setTD(atrBytes[index]);
index++;
}
mInterfaceBytes.add(interfaceByte);
Byte newTD = interfaceByte.getTD();
if (VDBG) log("index=" + index + ", " + toString());
if (newTD == null) {
break;
}
lastTD = newTD;
// Parse the T values from all the TD, here we only check whether T is equal to any
// other values other than 0, since the check byte can be absent only when T is
// equal to 0.
if ((lastTD & T_MASK) != 0) {
mOnlyTEqualsZero = false;
}
}
return index;
}
private int parseHistoricalBytes(byte[] atrBytes, int index) {
int length = mFormatByte & T_MASK;
if (length + index > atrBytes.length) {
loge("Failed to read the historical bytes.");
return -1;
}
if (length > 0) {
mHistoricalBytes = HistoricalBytes.parseHistoricalBytes(atrBytes, index, length);
}
return index + length;
}
private int parseCheckBytes(byte[] atrBytes, int index) {
if (index < atrBytes.length) {
mCheckByte = atrBytes[index];
index++;
} else {
if (!mOnlyTEqualsZero) {
loge("Check byte must be present because T equals to values other than 0.");
return -1;
} else {
log("Check byte can be absent because T=0.");
}
}
return index;
}
private boolean parseAtrString(String atr) {
if (atr == null) {
loge("The input ATR string can not be null");
return false;
}
if (atr.length() % 2 != 0) {
loge("The length of input ATR string " + atr.length() + " is not even.");
return false;
}
if (atr.length() < 4) {
loge("Valid ATR string must at least contains TS and T0.");
return false;
}
byte[] atrBytes = IccUtils.hexStringToBytes(atr);
if (atrBytes == null) {
return false;
}
int index = parseConventionByte(atrBytes, 0);
if (index == -1) {
return false;
}
index = parseFormatByte(atrBytes, index);
if (index == -1) {
return false;
}
index = parseInterfaceBytes(atrBytes, index);
if (index == -1) {
return false;
}
index = parseHistoricalBytes(atrBytes, index);
if (index == -1) {
return false;
}
index = parseCheckBytes(atrBytes, index);
if (index == -1) {
return false;
}
if (index != atrBytes.length) {
loge("Unexpected bytes after the check byte.");
return false;
}
log("Successfully parsed the ATR string " + atr + " into " + toString());
checkIsEuiccSupported();
return true;
}
/**
* This class holds the interface bytes.
*/
public static class InterfaceByte {
private Byte mTA;
private Byte mTB;
private Byte mTC;
private Byte mTD;
@Nullable
public Byte getTA() {
return mTA;
}
@Nullable
public Byte getTB() {
return mTB;
}
@Nullable
public Byte getTC() {
return mTC;
}
@Nullable
public Byte getTD() {
return mTD;
}
public void setTA(Byte tA) {
mTA = tA;
}
public void setTB(Byte tB) {
mTB = tB;
}
public void setTC(Byte tC) {
mTC = tC;
}
public void setTD(Byte tD) {
mTD = tD;
}
private InterfaceByte() {
mTA = null;
mTB = null;
mTC = null;
mTD = null;
}
@VisibleForTesting
public InterfaceByte(Byte tA, Byte tB, Byte tC, Byte tD) {
this.mTA = tA;
this.mTB = tB;
this.mTC = tC;
this.mTD = tD;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
InterfaceByte ib = (InterfaceByte) o;
return (Objects.equals(mTA, ib.getTA())
&& Objects.equals(mTB, ib.getTB())
&& Objects.equals(mTC, ib.getTC())
&& Objects.equals(mTD, ib.getTD()));
}
@Override
public int hashCode() {
return Objects.hash(mTA, mTB, mTC, mTD);
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("{");
sb.append("TA=").append(byteToStringHex(mTA)).append(",");
sb.append("TB=").append(byteToStringHex(mTB)).append(",");
sb.append("TC=").append(byteToStringHex(mTC)).append(",");
sb.append("TD=").append(byteToStringHex(mTD));
sb.append("}");
return sb.toString();
}
};
private static void log(String msg) {
Rlog.d(TAG, msg);
}
private static void loge(String msg) {
Rlog.e(TAG, msg);
}
public byte getConventionByte() {
return mIsDirectConvention ? DIRECT_CONVENTION : INVERSE_CONVENTION;
}
public byte getFormatByte() {
return mFormatByte;
}
public List<InterfaceByte> getInterfaceBytes() {
return mInterfaceBytes;
}
@Nullable
public HistoricalBytes getHistoricalBytes() {
return mHistoricalBytes;
}
@Nullable
public Byte getCheckByte() {
return mCheckByte;
}
public boolean isEuiccSupported() {
return mIsEuiccSupported;
}
/** Return whether the extended LC & LE is supported. */
public boolean isExtendedApduSupported() {
if (mHistoricalBytes == null) {
return false;
}
byte[] cardCapabilities = mHistoricalBytes.getValue(TAG_CARD_CAPABILITIES);
if (cardCapabilities == null || cardCapabilities.length < 3) {
return false;
}
if (mIsDirectConvention) {
return (cardCapabilities[EXTENDED_APDU_INDEX] & B7_MASK) > 0;
} else {
return (cardCapabilities[EXTENDED_APDU_INDEX] & B2_MASK) > 0;
}
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("AnswerToReset:{");
sb.append("mConventionByte=")
.append(IccUtils.byteToHex(getConventionByte())).append(",");
sb.append("mFormatByte=").append(byteToStringHex(mFormatByte)).append(",");
sb.append("mInterfaceBytes={");
for (InterfaceByte ib : mInterfaceBytes) {
sb.append(ib.toString());
}
sb.append("},");
sb.append("mHistoricalBytes={");
if (mHistoricalBytes != null) {
for (byte b : mHistoricalBytes.getRawData()) {
sb.append(IccUtils.byteToHex(b)).append(",");
}
}
sb.append("},");
sb.append("mCheckByte=").append(byteToStringHex(mCheckByte));
sb.append("}");
return sb.toString();
}
/**
* Dump
*/
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("AnswerToReset:");
pw.println(toString());
pw.flush();
}
}