/* * 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 B7_MASK = 0x40; private static final int B2_MASK = 0x02; public static final byte EUICC_SUPPORTED = (byte) 0x82; 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 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 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 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 nodes, byte category) { mRawData = rawData; mNodes = nodes; mCategory = category; } private static int parseLtvNode( int index, ArrayMap 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 value of the first tB after T=15 is 82. 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() == EUICC_SUPPORTED) { 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 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(); } }