| /* |
| * Copyright (C) 2014 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.content.Intent; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.content.pm.Signature; |
| import android.os.AsyncResult; |
| import android.os.Binder; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.telephony.Rlog; |
| import android.telephony.TelephonyManager; |
| |
| import com.android.internal.telephony.CommandsInterface; |
| import com.android.internal.telephony.uicc.IccUtils; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.lang.IllegalArgumentException; |
| import java.lang.IndexOutOfBoundsException; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.X509Certificate; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| /** |
| * Class that reads and stores the carrier privileged rules from the UICC. |
| * |
| * The rules are read when the class is created, hence it should only be created |
| * after the UICC can be read. And it should be deleted when a UICC is changed. |
| * |
| * The spec for the rules: |
| * GP Secure Element Access Control: |
| * http://www.globalplatform.org/specifications/review/GPD_SE_Access_Control_v1.0.20.pdf |
| * Extension spec: |
| * https://code.google.com/p/seek-for-android/ |
| * |
| * |
| * TODO: Notifications. |
| * |
| * {@hide} |
| */ |
| public class UiccCarrierPrivilegeRules extends Handler { |
| private static final String LOG_TAG = "UiccCarrierPrivilegeRules"; |
| |
| private static final String AID = "A00000015141434C00"; |
| private static final int CLA = 0x80; |
| private static final int COMMAND = 0xCA; |
| private static final int P1 = 0xFF; |
| private static final int P2 = 0x40; |
| private static final int P2_EXTENDED_DATA = 0x60; |
| private static final int P3 = 0x00; |
| private static final String DATA = ""; |
| |
| /* |
| * Rules format: |
| * ALL_REF_AR_DO = TAG_ALL_REF_AR_DO + len + [REF_AR_DO]*n |
| * REF_AR_DO = TAG_REF_AR_DO + len + REF-DO + AR-DO |
| * |
| * REF_DO = TAG_REF_DO + len + DEVICE_APP_ID_REF_DO + (optional) PKG_REF_DO |
| * AR_DO = TAG_AR_DO + len + PERM_AR_DO |
| * |
| * DEVICE_APP_ID_REF_DO = TAG_DEVICE_APP_ID_REF_DO + len + sha256 hexstring of cert |
| * PKG_REF_DO = TAG_PKG_REF_DO + len + package name |
| * PERM_AR_DO = TAG_PERM_AR_DO + len + detailed permission (8 bytes) |
| * |
| * Data objects hierarchy by TAG: |
| * FF40 |
| * E2 |
| * E1 |
| * C1 |
| * CA |
| * E3 |
| * DB |
| */ |
| // Values from the data standard. |
| private static final String TAG_ALL_REF_AR_DO = "FF40"; |
| private static final String TAG_REF_AR_DO = "E2"; |
| private static final String TAG_REF_DO = "E1"; |
| private static final String TAG_DEVICE_APP_ID_REF_DO = "C1"; |
| private static final String TAG_PKG_REF_DO = "CA"; |
| private static final String TAG_AR_DO = "E3"; |
| private static final String TAG_PERM_AR_DO = "DB"; |
| |
| private static final int EVENT_OPEN_LOGICAL_CHANNEL_DONE = 1; |
| private static final int EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE = 2; |
| private static final int EVENT_CLOSE_LOGICAL_CHANNEL_DONE = 3; |
| |
| // State of the object. |
| private static final int STATE_LOADING = 0; |
| private static final int STATE_LOADED = 1; |
| private static final int STATE_ERROR = 2; |
| |
| // Describes a single rule. |
| private static class AccessRule { |
| public byte[] certificateHash; |
| public String packageName; |
| public long accessType; // This bit is not currently used, but reserved for future use. |
| |
| AccessRule(byte[] certificateHash, String packageName, long accessType) { |
| this.certificateHash = certificateHash; |
| this.packageName = packageName; |
| this.accessType = accessType; |
| } |
| |
| boolean matches(byte[] certHash, String packageName) { |
| return certHash != null && Arrays.equals(this.certificateHash, certHash) && |
| (this.packageName == null || this.packageName.equals(packageName)); |
| } |
| |
| @Override |
| public String toString() { |
| return "cert: " + IccUtils.bytesToHexString(certificateHash) + " pkg: " + |
| packageName + " access: " + accessType; |
| } |
| } |
| |
| // Used for parsing the data from the UICC. |
| private static class TLV { |
| private static final int SINGLE_BYTE_MAX_LENGTH = 0x80; |
| private String tag; |
| // Length encoding is in GPC_Specification_2.2.1: 11.1.5 APDU Message and Data Length. |
| // Length field could be either 1 byte if length < 128, or multiple bytes with first byte |
| // specifying how many bytes are used for length, followed by length bytes. |
| // Bytes for the length field, in ASCII HEX string form. |
| private String lengthBytes; |
| // Decoded length as integer. |
| private Integer length; |
| private String value; |
| |
| public TLV(String tag) { |
| this.tag = tag; |
| } |
| |
| public String parseLength(String data) { |
| int offset = tag.length(); |
| int firstByte = Integer.parseInt(data.substring(offset, offset + 2), 16); |
| // TODO: remove second condition before launch. b/18012893 |
| if (firstByte < SINGLE_BYTE_MAX_LENGTH || (offset + 2 + firstByte * 2 == data.length())) { |
| length = firstByte * 2; |
| lengthBytes = data.substring(offset, offset + 2); |
| } else { |
| int numBytes = firstByte - SINGLE_BYTE_MAX_LENGTH; |
| length = Integer.parseInt(data.substring(offset + 2, offset + 2 + numBytes * 2), 16) * 2; |
| lengthBytes = data.substring(offset, offset + 2 + numBytes * 2); |
| } |
| Rlog.d(LOG_TAG, "TLV parseLength length=" + length + "lenghtBytes: " + lengthBytes); |
| return lengthBytes; |
| } |
| |
| public String parse(String data, boolean shouldConsumeAll) { |
| Rlog.d(LOG_TAG, "Parse TLV: " + tag); |
| if (!data.startsWith(tag)) { |
| throw new IllegalArgumentException("Tags don't match."); |
| } |
| int index = tag.length(); |
| if (index + 2 > data.length()) { |
| throw new IllegalArgumentException("No length."); |
| } |
| |
| parseLength(data); |
| index += lengthBytes.length(); |
| |
| Rlog.d(LOG_TAG, "index="+index+" length="+length+"data.length="+data.length()); |
| int remainingLength = data.length() - (index + length); |
| if (remainingLength < 0) { |
| throw new IllegalArgumentException("Not enough data."); |
| } |
| if (shouldConsumeAll && (remainingLength != 0)) { |
| throw new IllegalArgumentException("Did not consume all."); |
| } |
| value = data.substring(index, index + length); |
| |
| Rlog.d(LOG_TAG, "Got TLV: " + tag + "," + length + "," + value); |
| |
| return data.substring(index + length); |
| } |
| } |
| |
| private UiccCard mUiccCard; // Parent |
| private AtomicInteger mState; |
| private List<AccessRule> mAccessRules; |
| private String mRules; |
| private Message mLoadedCallback; |
| private String mStatusMessage; // Only used for debugging. |
| private int mChannelId; // Channel Id for communicating with UICC. |
| |
| public UiccCarrierPrivilegeRules(UiccCard uiccCard, Message loadedCallback) { |
| Rlog.d(LOG_TAG, "Creating UiccCarrierPrivilegeRules"); |
| mUiccCard = uiccCard; |
| mState = new AtomicInteger(STATE_LOADING); |
| mStatusMessage = "Not loaded."; |
| mLoadedCallback = loadedCallback; |
| mRules = ""; |
| |
| // Start loading the rules. |
| mUiccCard.iccOpenLogicalChannel(AID, |
| obtainMessage(EVENT_OPEN_LOGICAL_CHANNEL_DONE, null)); |
| } |
| |
| /** |
| * Returns true if the carrier privilege rules have finished loading. |
| */ |
| public boolean areCarrierPriviligeRulesLoaded() { |
| return mState.get() != STATE_LOADING; |
| } |
| |
| /** |
| * Returns the status of the carrier privileges for the input certificate and package name. |
| * |
| * @param signature The signature of the certificate. |
| * @param packageName name of the package. |
| * @return Access status. |
| */ |
| public int getCarrierPrivilegeStatus(Signature signature, String packageName) { |
| Rlog.d(LOG_TAG, "hasCarrierPrivileges: " + signature + " : " + packageName); |
| int state = mState.get(); |
| if (state == STATE_LOADING) { |
| Rlog.d(LOG_TAG, "Rules not loaded."); |
| return TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED; |
| } else if (state == STATE_ERROR) { |
| Rlog.d(LOG_TAG, "Error loading rules."); |
| return TelephonyManager.CARRIER_PRIVILEGE_STATUS_ERROR_LOADING_RULES; |
| } |
| |
| // SHA-1 is for backward compatible support only, strongly discouraged for new use. |
| byte[] certHash = getCertHash(signature, "SHA-1"); |
| byte[] certHash256 = getCertHash(signature, "SHA-256"); |
| Rlog.d(LOG_TAG, "Checking SHA1: " + IccUtils.bytesToHexString(certHash) + " : " + packageName); |
| Rlog.d(LOG_TAG, "Checking SHA256: " + IccUtils.bytesToHexString(certHash256) + " : " + packageName); |
| for (AccessRule ar : mAccessRules) { |
| if (ar.matches(certHash, packageName) || ar.matches(certHash256, packageName)) { |
| Rlog.d(LOG_TAG, "Match found!"); |
| return TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS; |
| } |
| } |
| |
| Rlog.d(LOG_TAG, "No matching rule found. Returning false."); |
| return TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS; |
| } |
| |
| /** |
| * Returns the status of the carrier privileges for the input package name. |
| * |
| * @param packageManager PackageManager for getting signatures. |
| * @param packageName name of the package. |
| * @return Access status. |
| */ |
| public int getCarrierPrivilegeStatus(PackageManager packageManager, String packageName) { |
| try { |
| PackageInfo pInfo = packageManager.getPackageInfo(packageName, |
| PackageManager.GET_SIGNATURES); |
| Signature[] signatures = pInfo.signatures; |
| for (Signature sig : signatures) { |
| int accessStatus = getCarrierPrivilegeStatus(sig, pInfo.packageName); |
| if (accessStatus != TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS) { |
| return accessStatus; |
| } |
| } |
| } catch (PackageManager.NameNotFoundException ex) { |
| Rlog.e(LOG_TAG, "NameNotFoundException", ex); |
| } |
| return TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS; |
| } |
| |
| /** |
| * Returns the status of the carrier privileges for the caller of the current transaction. |
| * |
| * @param packageManager PackageManager for getting signatures and package names. |
| * @return Access status. |
| */ |
| public int getCarrierPrivilegeStatusForCurrentTransaction(PackageManager packageManager) { |
| String[] packages = packageManager.getPackagesForUid(Binder.getCallingUid()); |
| |
| for (String pkg : packages) { |
| int accessStatus = getCarrierPrivilegeStatus(packageManager, pkg); |
| if (accessStatus != TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS) { |
| return accessStatus; |
| } |
| } |
| return TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS; |
| } |
| |
| /** |
| * Returns the package name of the carrier app that should handle the input intent. |
| * |
| * @param packageManager PackageManager for getting receivers. |
| * @param intent Intent that will be sent. |
| * @return list of carrier app package names that can handle the intent. |
| * Returns null if there is an error and an empty list if there |
| * are no matching packages. |
| */ |
| public List<String> getCarrierPackageNamesForIntent( |
| PackageManager packageManager, Intent intent) { |
| List<String> packages = new ArrayList<String>(); |
| List<ResolveInfo> receivers = new ArrayList<ResolveInfo>(); |
| receivers.addAll(packageManager.queryBroadcastReceivers(intent, 0)); |
| receivers.addAll(packageManager.queryIntentContentProviders(intent, 0)); |
| receivers.addAll(packageManager.queryIntentActivities(intent, 0)); |
| receivers.addAll(packageManager.queryIntentServices(intent, 0)); |
| |
| for (ResolveInfo resolveInfo : receivers) { |
| String packageName = getPackageName(resolveInfo); |
| if (packageName == null) { |
| continue; |
| } |
| |
| int status = getCarrierPrivilegeStatus(packageManager, packageName); |
| if (status == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) { |
| packages.add(packageName); |
| } else if (status != TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS) { |
| // Any status apart from HAS_ACCESS and NO_ACCESS is considered an error. |
| return null; |
| } |
| } |
| |
| return packages; |
| } |
| |
| @Nullable |
| private String getPackageName(ResolveInfo resolveInfo) { |
| if (resolveInfo.activityInfo != null) { |
| return resolveInfo.activityInfo.packageName; |
| } else if (resolveInfo.serviceInfo != null) { |
| return resolveInfo.serviceInfo.packageName; |
| } else if (resolveInfo.providerInfo != null) { |
| return resolveInfo.providerInfo.packageName; |
| } |
| return null; |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| AsyncResult ar; |
| |
| switch (msg.what) { |
| |
| case EVENT_OPEN_LOGICAL_CHANNEL_DONE: |
| Rlog.d(LOG_TAG, "EVENT_OPEN_LOGICAL_CHANNEL_DONE"); |
| ar = (AsyncResult) msg.obj; |
| if (ar.exception == null && ar.result != null) { |
| mChannelId = ((int[]) ar.result)[0]; |
| mUiccCard.iccTransmitApduLogicalChannel(mChannelId, CLA, COMMAND, P1, P2, P3, DATA, |
| obtainMessage(EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE, new Integer(mChannelId))); |
| } else { |
| updateState(STATE_ERROR, "Error opening channel"); |
| } |
| break; |
| |
| case EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE: |
| Rlog.d(LOG_TAG, "EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE"); |
| ar = (AsyncResult) msg.obj; |
| if (ar.exception == null && ar.result != null) { |
| IccIoResult response = (IccIoResult) ar.result; |
| if (response.sw1 == 0x90 && response.sw2 == 0x00 && |
| response.payload != null && response.payload.length > 0) { |
| try { |
| mRules += IccUtils.bytesToHexString(response.payload).toUpperCase(Locale.US); |
| if (isDataComplete()) { |
| mAccessRules = parseRules(mRules); |
| updateState(STATE_LOADED, "Success!"); |
| } else { |
| mUiccCard.iccTransmitApduLogicalChannel(mChannelId, CLA, COMMAND, P1, P2_EXTENDED_DATA, P3, DATA, |
| obtainMessage(EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE, new Integer(mChannelId))); |
| break; |
| } |
| } catch (IllegalArgumentException ex) { |
| updateState(STATE_ERROR, "Error parsing rules: " + ex); |
| } catch (IndexOutOfBoundsException ex) { |
| updateState(STATE_ERROR, "Error parsing rules: " + ex); |
| } |
| } else { |
| String errorMsg = "Invalid response: payload=" + response.payload + |
| " sw1=" + response.sw1 + " sw2=" + response.sw2; |
| updateState(STATE_ERROR, errorMsg); |
| } |
| } else { |
| updateState(STATE_ERROR, "Error reading value from SIM."); |
| } |
| |
| mUiccCard.iccCloseLogicalChannel(mChannelId, obtainMessage( |
| EVENT_CLOSE_LOGICAL_CHANNEL_DONE)); |
| mChannelId = -1; |
| break; |
| |
| case EVENT_CLOSE_LOGICAL_CHANNEL_DONE: |
| Rlog.d(LOG_TAG, "EVENT_CLOSE_LOGICAL_CHANNEL_DONE"); |
| break; |
| |
| default: |
| Rlog.e(LOG_TAG, "Unknown event " + msg.what); |
| } |
| } |
| |
| /* |
| * Check if all rule bytes have been read from UICC. |
| * For long payload, we need to fetch it repeatly before start parsing it. |
| */ |
| private boolean isDataComplete() { |
| Rlog.d(LOG_TAG, "isDataComplete mRules:" + mRules); |
| if (mRules.startsWith(TAG_ALL_REF_AR_DO)) { |
| TLV allRules = new TLV(TAG_ALL_REF_AR_DO); |
| String lengthBytes = allRules.parseLength(mRules); |
| Rlog.d(LOG_TAG, "isDataComplete lengthBytes: " + lengthBytes); |
| if (mRules.length() == TAG_ALL_REF_AR_DO.length() + lengthBytes.length() + |
| allRules.length) { |
| Rlog.d(LOG_TAG, "isDataComplete yes"); |
| return true; |
| } else { |
| Rlog.d(LOG_TAG, "isDataComplete no"); |
| return false; |
| } |
| } else { |
| throw new IllegalArgumentException("Tags don't match."); |
| } |
| } |
| |
| /* |
| * Parses the rules from the input string. |
| */ |
| private static List<AccessRule> parseRules(String rules) { |
| Rlog.d(LOG_TAG, "Got rules: " + rules); |
| |
| TLV allRefArDo = new TLV(TAG_ALL_REF_AR_DO); //FF40 |
| allRefArDo.parse(rules, true); |
| |
| String arDos = allRefArDo.value; |
| List<AccessRule> accessRules = new ArrayList<AccessRule>(); |
| while (!arDos.isEmpty()) { |
| TLV refArDo = new TLV(TAG_REF_AR_DO); //E2 |
| arDos = refArDo.parse(arDos, false); |
| AccessRule accessRule = parseRefArdo(refArDo.value); |
| if (accessRule != null) { |
| accessRules.add(accessRule); |
| } else { |
| Rlog.e(LOG_TAG, "Skip unrecognized rule." + refArDo.value); |
| } |
| } |
| return accessRules; |
| } |
| |
| /* |
| * Parses a single rule. |
| */ |
| private static AccessRule parseRefArdo(String rule) { |
| Rlog.d(LOG_TAG, "Got rule: " + rule); |
| |
| String certificateHash = null; |
| String packageName = null; |
| String tmp = null; |
| long accessType = 0; |
| |
| while (!rule.isEmpty()) { |
| if (rule.startsWith(TAG_REF_DO)) { |
| TLV refDo = new TLV(TAG_REF_DO); //E1 |
| rule = refDo.parse(rule, false); |
| |
| // Skip unrelated rules. |
| if (!refDo.value.startsWith(TAG_DEVICE_APP_ID_REF_DO)) { |
| return null; |
| } |
| |
| TLV deviceDo = new TLV(TAG_DEVICE_APP_ID_REF_DO); //C1 |
| tmp = deviceDo.parse(refDo.value, false); |
| certificateHash = deviceDo.value; |
| |
| if (!tmp.isEmpty()) { |
| if (!tmp.startsWith(TAG_PKG_REF_DO)) { |
| return null; |
| } |
| TLV pkgDo = new TLV(TAG_PKG_REF_DO); //CA |
| pkgDo.parse(tmp, true); |
| packageName = new String(IccUtils.hexStringToBytes(pkgDo.value)); |
| } else { |
| packageName = null; |
| } |
| } else if (rule.startsWith(TAG_AR_DO)) { |
| TLV arDo = new TLV(TAG_AR_DO); //E3 |
| rule = arDo.parse(rule, false); |
| |
| // Skip unrelated rules. |
| if (!arDo.value.startsWith(TAG_PERM_AR_DO)) { |
| return null; |
| } |
| |
| TLV permDo = new TLV(TAG_PERM_AR_DO); //DB |
| permDo.parse(arDo.value, true); |
| Rlog.e(LOG_TAG, permDo.value); |
| } else { |
| // Spec requires it must be either TAG_REF_DO or TAG_AR_DO. |
| throw new RuntimeException("Invalid Rule type"); |
| } |
| } |
| |
| Rlog.e(LOG_TAG, "Adding: " + certificateHash + " : " + packageName + " : " + accessType); |
| |
| AccessRule accessRule = new AccessRule(IccUtils.hexStringToBytes(certificateHash), |
| packageName, accessType); |
| Rlog.e(LOG_TAG, "Parsed rule: " + accessRule); |
| return accessRule; |
| } |
| |
| /* |
| * Converts a Signature into a Certificate hash usable for comparison. |
| */ |
| private static byte[] getCertHash(Signature signature, String algo) { |
| try { |
| MessageDigest md = MessageDigest.getInstance(algo); |
| return md.digest(signature.toByteArray()); |
| } catch (NoSuchAlgorithmException ex) { |
| Rlog.e(LOG_TAG, "NoSuchAlgorithmException: " + ex); |
| } |
| return null; |
| } |
| |
| /* |
| * Updates the state and notifies the UiccCard that the rules have finished loading. |
| */ |
| private void updateState(int newState, String statusMessage) { |
| mState.set(newState); |
| if (mLoadedCallback != null) { |
| mLoadedCallback.sendToTarget(); |
| } |
| |
| mStatusMessage = statusMessage; |
| Rlog.e(LOG_TAG, mStatusMessage); |
| } |
| |
| /** |
| * Dumps info to Dumpsys - useful for debugging. |
| */ |
| public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| pw.println("UiccCarrierPrivilegeRules: " + this); |
| pw.println(" mState=" + getStateString(mState.get())); |
| pw.println(" mStatusMessage='" + mStatusMessage + "'"); |
| if (mAccessRules != null) { |
| pw.println(" mAccessRules: "); |
| for (AccessRule ar : mAccessRules) { |
| pw.println(" rule='" + ar + "'"); |
| } |
| } else { |
| pw.println(" mAccessRules: null"); |
| } |
| pw.flush(); |
| } |
| |
| /* |
| * Converts state into human readable format. |
| */ |
| private String getStateString(int state) { |
| switch (state) { |
| case STATE_LOADING: |
| return "STATE_LOADING"; |
| case STATE_LOADED: |
| return "STATE_LOADED"; |
| case STATE_ERROR: |
| return "STATE_ERROR"; |
| default: |
| return "UNKNOWN"; |
| } |
| } |
| } |