/* * Copyright (C) 2006 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.NonNull; import android.compat.annotation.UnsupportedAppUsage; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.graphics.Bitmap; import android.graphics.Color; import android.os.Build; import android.telephony.UiccPortInfo; import android.text.TextUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.GsmAlphabet; import com.android.telephony.Rlog; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.util.List; /** * Various methods, useful for dealing with SIM data. */ public class IccUtils { static final String LOG_TAG="IccUtils"; // 3GPP specification constants // Spec reference TS 31.102 section 4.2.16 @VisibleForTesting static final int FPLMN_BYTE_SIZE = 3; // ICCID used for tests by some OEMs public static final String TEST_ICCID = UiccPortInfo.ICCID_REDACTED; // 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 * * Assumes left-justified field that may be padded right with 0xf * values. * * Stops on invalid BCD value, returning string so far */ @UnsupportedAppUsage public static String bcdToString(byte[] data, int offset, int length) { StringBuilder ret = new StringBuilder(length*2); for (int i = offset ; i < offset + length ; i++) { int v; v = data[i] & 0xf; if (v > 9) break; ret.append((char)('0' + v)); v = (data[i] >> 4) & 0xf; // Some PLMNs have 'f' as high nibble, ignore it if (v == 0xf) continue; if (v > 9) break; ret.append((char)('0' + v)); } return ret.toString(); } /** * 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) { bcdToBytes(bcd, bytes, 0); } /** * 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. * @param offset the offset into the bytes[] to fill the data */ public static void bcdToBytes(String bcd, byte[] bytes, int offset) { if (bcd.length() % 2 != 0) { bcd += "0"; } int size = Math.min((bytes.length - offset) * 2, bcd.length()); for (int i = 0, j = offset; 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' */ public static String bcdPlmnToString(byte[] data, int offset) { if (offset + 3 > data.length) { return null; } byte[] trans = new byte[3]; trans[0] = (byte) ((data[0 + offset] << 4) | ((data[0 + offset] >> 4) & 0xF)); trans[1] = (byte) ((data[1 + offset] << 4) | (data[2 + offset] & 0xF)); trans[2] = (byte) ((data[2 + offset] & 0xF0) | ((data[1 + offset] >> 4) & 0xF)); String ret = bytesToHexString(trans); // For a valid plmn we trim all character 'F' if (ret.contains("F")) { ret = ret.replaceAll("F", ""); } return ret; } /** * Convert a 5 or 6 - digit PLMN string to a nibble-swizzled encoding as per 24.008 10.5.1.3 * * @param plmn the PLMN to convert * @param data a byte array for the output * @param offset the offset into data to start writing */ public static void stringToBcdPlmn(final String plmn, byte[] data, int offset) { char digit6 = (plmn.length() > 5) ? plmn.charAt(5) : 'F'; data[offset] = (byte) (charToByte(plmn.charAt(1)) << 4 | charToByte(plmn.charAt(0))); data[offset + 1] = (byte) (charToByte(digit6) << 4 | charToByte(plmn.charAt(2))); data[offset + 2] = (byte) (charToByte(plmn.charAt(4)) << 4 | charToByte(plmn.charAt(3))); } /** * Some fields (like ICC ID) in GSM SIMs are stored as nibble-swizzled BCH */ public static String bchToString(byte[] data, int offset, int length) { StringBuilder ret = new StringBuilder(length*2); for (int i = offset ; i < offset + length ; i++) { int v; v = data[i] & 0xf; ret.append(HEX_CHARS[v]); v = (data[i] >> 4) & 0xf; ret.append(HEX_CHARS[v]); } return ret.toString(); } /** * Decode cdma byte into String. */ @UnsupportedAppUsage public static String cdmaBcdToString(byte[] data, int offset, int length) { StringBuilder ret = new StringBuilder(length); int count = 0; for (int i = offset; count < length; i++) { int v; v = data[i] & 0xf; if (v > 9) v = 0; ret.append((char)('0' + v)); if (++count == length) break; v = (data[i] >> 4) & 0xf; if (v > 9) v = 0; ret.append((char)('0' + v)); ++count; } return ret.toString(); } /** * Decodes a GSM-style BCD byte, returning an int ranging from 0-99. * * In GSM land, the least significant BCD digit is stored in the most * significant nibble. * * Out-of-range digits are treated as 0 for the sake of the time stamp, * because of this: * * TS 23.040 section 9.2.3.11 * "if the MS receives a non-integer value in the SCTS, it shall * assume the digit is set to 0 but shall store the entire field * exactly as received" */ @UnsupportedAppUsage public static int gsmBcdByteToInt(byte b) { int ret = 0; // treat out-of-range BCD values as 0 if ((b & 0xf0) <= 0x90) { ret = (b >> 4) & 0xf; } if ((b & 0x0f) <= 0x09) { ret += (b & 0xf) * 10; } return ret; } /** * Decodes a CDMA style BCD byte like {@link #gsmBcdByteToInt}, but * opposite nibble format. The least significant BCD digit * is in the least significant nibble and the most significant * is in the most significant nibble. */ @UnsupportedAppUsage public static int cdmaBcdByteToInt(byte b) { int ret = 0; // treat out-of-range BCD values as 0 if ((b & 0xf0) <= 0x90) { ret = ((b >> 4) & 0xf) * 10; } if ((b & 0x0f) <= 0x09) { ret += (b & 0xf); } return ret; } /** * Encodes a string to be formatted like the EF[ADN] alpha identifier. * *
See javadoc for {@link #adnStringFieldToString(byte[], int, int)} for more details on * the relevant specs. * *
This will attempt to encode using the GSM 7-bit alphabet but will fallback to UCS-2 if
* there are characters that are not supported by it.
*
* @return the encoded string including the prefix byte necessary to identify the encoding.
* @see #adnStringFieldToString(byte[], int, int)
*/
@NonNull
public static byte[] stringToAdnStringField(@NonNull String alphaTag) {
int septets = GsmAlphabet.countGsmSeptetsUsingTables(alphaTag, false, 0, 0);
if (septets != -1) {
byte[] ret = new byte[septets];
GsmAlphabet.stringToGsm8BitUnpackedField(alphaTag, ret, 0, ret.length);
return ret;
}
// Strictly speaking UCS-2 disallows surrogate characters but it's much more complicated to
// validate that the string contains only valid UCS-2 characters. Since the read path
// in most modern software will decode "UCS-2" by treating it as UTF-16 this should be fine
// (e.g. the adnStringFieldToString has done this for a long time on Android). Also there's
// already a precedent in SMS applications to ignore the UCS-2/UTF-16 distinction.
byte[] alphaTagBytes = alphaTag.getBytes(StandardCharsets.UTF_16BE);
byte[] ret = new byte[alphaTagBytes.length + 1];
// 0x80 tags the remaining bytes as UCS-2
ret[0] = (byte) 0x80;
System.arraycopy(alphaTagBytes, 0, ret, 1, alphaTagBytes.length);
return ret;
}
/**
* Decodes a string field that's formatted like the EF[ADN] alpha
* identifier
*
* From TS 51.011 10.5.1:
* Coding:
* this alpha tagging shall use either
* - the SMS default 7 bit coded alphabet as defined in
* TS 23.038 [12] with bit 8 set to 0. The alpha identifier
* shall be left justified. Unused bytes shall be set to 'FF'; or
* - one of the UCS2 coded options as defined in annex B.
*
* Annex B from TS 11.11 V8.13.0:
* 1) If the first octet in the alpha string is '80', then the
* remaining octets are 16 bit UCS2 characters ...
* 2) if the first octet in the alpha string is '81', then the
* second octet contains a value indicating the number of
* characters in the string, and the third octet contains an
* 8 bit number which defines bits 15 to 8 of a 16 bit
* base pointer, where bit 16 is set to zero and bits 7 to 1
* are also set to zero. These sixteen bits constitute a
* base pointer to a "half page" in the UCS2 code space, to be
* used with some or all of the remaining octets in the string.
* The fourth and subsequent octets contain codings as follows:
* If bit 8 of the octet is set to zero, the remaining 7 bits
* of the octet contain a GSM Default Alphabet character,
* whereas if bit 8 of the octet is set to one, then the
* remaining seven bits are an offset value added to the
* 16 bit base pointer defined earlier...
* 3) If the first octet of the alpha string is set to '82', then
* the second octet contains a value indicating the number of
* characters in the string, and the third and fourth octets
* contain a 16 bit number which defines the complete 16 bit
* base pointer to a "half page" in the UCS2 code space...
*/
@UnsupportedAppUsage
public static String
adnStringFieldToString(byte[] data, int offset, int length) {
if (length == 0) {
return "";
}
if (length >= 1) {
if (data[offset] == (byte) 0x80) {
int ucslen = (length - 1) / 2;
String ret = null;
try {
ret = new String(data, offset + 1, ucslen * 2, "utf-16be");
} catch (UnsupportedEncodingException ex) {
Rlog.e(LOG_TAG, "implausible UnsupportedEncodingException",
ex);
}
if (ret != null) {
// trim off trailing FFFF characters
ucslen = ret.length();
while (ucslen > 0 && ret.charAt(ucslen - 1) == '\uFFFF')
ucslen--;
return ret.substring(0, ucslen);
}
}
}
boolean isucs2 = false;
char base = '\0';
int len = 0;
if (length >= 3 && data[offset] == (byte) 0x81) {
len = data[offset + 1] & 0xFF;
if (len > length - 3)
len = length - 3;
base = (char) ((data[offset + 2] & 0xFF) << 7);
offset += 3;
isucs2 = true;
} else if (length >= 4 && data[offset] == (byte) 0x82) {
len = data[offset + 1] & 0xFF;
if (len > length - 4)
len = length - 4;
base = (char) (((data[offset + 2] & 0xFF) << 8) |
(data[offset + 3] & 0xFF));
offset += 4;
isucs2 = true;
}
if (isucs2) {
StringBuilder ret = new StringBuilder();
while (len > 0) {
// UCS2 subset case
if (data[offset] < 0) {
ret.append((char) (base + (data[offset] & 0x7F)));
offset++;
len--;
}
// GSM character set case
int count = 0;
while (count < len && data[offset + count] >= 0)
count++;
ret.append(GsmAlphabet.gsm8BitUnpackedToString(data,
offset, count));
offset += count;
len -= count;
}
return ret.toString();
}
Resources resource = Resources.getSystem();
String defaultCharset = "";
try {
defaultCharset =
resource.getString(com.android.internal.R.string.gsm_alphabet_default_charset);
} catch (NotFoundException e) {
// Ignore Exception and defaultCharset is set to a empty string.
}
return GsmAlphabet.gsm8BitUnpackedToString(data, offset, length, defaultCharset.trim());
}
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static int
hexCharToInt(char c) {
if (c >= '0' && c <= '9') return (c - '0');
if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
throw new RuntimeException ("invalid hex char '" + c + "'");
}
/**
* Converts a hex String to a byte array.
*
* @param s A string of hexadecimal characters, must be an even number of
* chars long
*
* @return byte array representation
*
* @throws RuntimeException on invalid format
*/
@UnsupportedAppUsage
public static byte[]
hexStringToBytes(String s) {
byte[] ret;
if (s == null) return null;
int sz = s.length();
ret = new byte[sz/2];
for (int i=0 ; i