diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2021-02-24 17:45:27 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2021-02-24 17:45:27 +0000 |
commit | 1e7b0e68f46e181d1982ccbc32b9177fc618e518 (patch) | |
tree | 1a74744acf9d4678bc5181e37dfe16246d08d7a0 | |
parent | f2fa93cfe74302592a5aa6db3965c4c8be333074 (diff) | |
parent | c047965e68acbfaaf84e4592a76ab8508bbdb5fd (diff) | |
download | telephony-1e7b0e68f46e181d1982ccbc32b9177fc618e518.tar.gz |
Merge "Cache SIM PIN for verification after unattended reboot." am: c047965e68
Original change: https://android-review.googlesource.com/c/platform/frameworks/opt/telephony/+/1590934
MUST ONLY BE SUBMITTED BY AUTOMERGER
Change-Id: I3a3f52a175d97ea1fb11eee586ccd8520efec44b
7 files changed, 1691 insertions, 1 deletions
diff --git a/proto/src/pin_storage.proto b/proto/src/pin_storage.proto new file mode 100644 index 0000000000..f572a212f4 --- /dev/null +++ b/proto/src/pin_storage.proto @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2020 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. + */ + +syntax = "proto2"; + +package telephonyPinStorage; + +option java_package = "com.android.internal.telephony"; +option java_outer_classname = "StoredPinProto"; + +// Stores information about PIN of a SIM card. +message StoredPin { + // Status of the PIN. + enum PinStatus { + // The PIN code is stored, but cannot be used for automatic verification. + AVAILABLE = 1; + + // The PIN code is stored and will be usable for automatic verification after the + // unattended reboot is completed. + REBOOT_READY = 2; + + // The PIN code is stored and can be used for automatic verification. + VERIFICATION_READY = 3; + } + + // ICCID of the SIM card + optional string iccid = 1; + + // PIN code + optional string pin = 2; + + // Slot number + optional int32 slot_id = 3; + + // Status of the PIN code + optional PinStatus status = 4; + + // Boot count when the proto was generated. + optional int32 boot_count = 5; +} + +// Stores the encrypted version of StoredPin. +message EncryptedPin { + // Encrypted StoredPin + optional bytes encrypted_stored_pin = 1; + + // Initialization vector + optional bytes iv = 2; +}
\ No newline at end of file diff --git a/src/java/com/android/internal/telephony/uicc/PinStorage.java b/src/java/com/android/internal/telephony/uicc/PinStorage.java new file mode 100644 index 0000000000..b7621d2e3f --- /dev/null +++ b/src/java/com/android/internal/telephony/uicc/PinStorage.java @@ -0,0 +1,1149 @@ +/* + * Copyright 2020 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 static android.security.keystore.KeyProperties.AUTH_DEVICE_CREDENTIAL; +import static android.security.keystore.KeyProperties.BLOCK_MODE_GCM; +import static android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE; +import static android.security.keystore.KeyProperties.KEY_ALGORITHM_AES; +import static android.security.keystore.KeyProperties.PURPOSE_DECRYPT; +import static android.security.keystore.KeyProperties.PURPOSE_ENCRYPT; + +import static com.android.internal.telephony.TelephonyStatsLog.PIN_STORAGE_EVENT; +import static com.android.internal.telephony.TelephonyStatsLog.PIN_STORAGE_EVENT__EVENT__CACHED_PIN_DISCARDED; +import static com.android.internal.telephony.TelephonyStatsLog.PIN_STORAGE_EVENT__EVENT__PIN_STORED_FOR_VERIFICATION; +import static com.android.internal.telephony.uicc.IccCardStatus.PinState.PINSTATE_ENABLED_NOT_VERIFIED; +import static com.android.internal.telephony.uicc.IccCardStatus.PinState.PINSTATE_ENABLED_VERIFIED; + +import android.annotation.Nullable; +import android.app.KeyguardManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.PersistableBundle; +import android.provider.Settings; +import android.security.keystore.KeyGenParameterSpec; +import android.telephony.CarrierConfigManager; +import android.telephony.TelephonyManager; +import android.telephony.TelephonyManager.SimState; +import android.util.Base64; +import android.util.SparseArray; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.PhoneFactory; +import com.android.internal.telephony.SubscriptionInfoUpdater; +import com.android.internal.telephony.TelephonyStatsLog; +import com.android.internal.telephony.nano.StoredPinProto.EncryptedPin; +import com.android.internal.telephony.nano.StoredPinProto.StoredPin; +import com.android.internal.telephony.nano.StoredPinProto.StoredPin.PinStatus; +import com.android.internal.telephony.uicc.IccCardStatus.PinState; +import com.android.internal.util.ArrayUtils; +import com.android.telephony.Rlog; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.security.KeyStore; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +/** + * This class stores the SIM PIN for automatic verification after an unattended reboot. + */ +public class PinStorage extends Handler { + private static final String TAG = "PinStorage"; + private static final boolean VDBG = false; // STOPSHIP if true + + /** + * Time duration in milliseconds to allow automatic PIN verification after reboot. All unused + * PINs are discarded when the timer expires. + */ + private static final int TIMER_VALUE_AFTER_OTA_MILLIS = 20_000; + + /** + * Time duration in milliseconds to reboot the device after {@code prepareUnattendedReboot} + * is invoked. After the time expires, a new invocation of {@code prepareUnattendedReboot} is + * required to perform the automatic PIN verification after reboot. + */ + private static final int TIMER_VALUE_BEFORE_OTA_MILLIS = 20_000; + + /** Minimum valid length of the ICCID. */ + private static final int MIN_ICCID_LENGTH = 12; + /** Minimum length of the SIM PIN, as per 3GPP TS 31.101. */ + private static final int MIN_PIN_LENGTH = 4; + /** Maximum length of the SIM PIN, as per 3GPP TS 31.101. */ + private static final int MAX_PIN_LENGTH = 8; + + // Variables related to the encryption of the SIM PIN. + private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore"; + private static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding"; + private static final int GCM_PARAMETER_TAG_BIT_LEN = 128; + private static final int SHORT_TERM_KEY_DURATION_MINUTES = 15; + + /** Alias of the long-term key that does not require user authentication. */ + private static final String KEYSTORE_ALIAS_LONG_TERM_ALWAYS = "PinStorage_longTerm_always_key"; + /** Alias of the user authentication blound long-term key. */ + private static final String KEYSTORE_ALIAS_LONG_TERM_USER_AUTH = "PinStorage_longTerm_ua_key"; + /** Alias of the short-term key (30 minutes) used before and after an unattended reboot. */ + private static final String KEYSTORE_ALIAS_SHORT_TERM = "PinStorage_shortTerm_key"; + + // Constants related to the storage of the encrypted SIM PIN to non-volatile memory. + // Data is stored in two separate files: + // - "available" is for the PIN(s) in AVAILABLE state and uses a key that does not expire + // - "reboot" is for the PIN(s) in other states and uses a short-term key (30 minutes) + private static final String SHARED_PREFS_NAME = "pinstorage_prefs"; + private static final String SHARED_PREFS_AVAILABLE_PIN_BASE_KEY = "encrypted_pin_available_"; + private static final String SHARED_PREFS_REBOOT_PIN_BASE_KEY = "encrypted_pin_reboot_"; + + // Events + private static final int ICC_CHANGED_EVENT = 1; + private static final int CARRIER_CONFIG_CHANGED_EVENT = 2; + private static final int TIMER_EXPIRATION_EVENT = 3; + private static final int USER_UNLOCKED_EVENT = 4; + private static final int SUPPLY_PIN_COMPLETE = 5; + + private final Context mContext; + private final int mBootCount; + private final KeyStore mKeyStore; + + private SecretKey mLongTermSecretKey; + private SecretKey mShortTermSecretKey; + + private boolean mIsDeviceSecure; + private boolean mIsDeviceLocked; + private boolean mLastCommitResult = true; + + /** Duration of the short-term key, in minutes. */ + @VisibleForTesting + public int mShortTermSecretKeyDurationMinutes; + + /** RAM storage is used on secure devices before the device is unlocked. */ + private final SparseArray<byte[]> mRamStorage; + + /** Receiver for the required intents. */ + private final BroadcastReceiver mCarrierConfigChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)) { + int slotId = intent.getIntExtra(CarrierConfigManager.EXTRA_SLOT_INDEX, -1); + sendMessage(obtainMessage(CARRIER_CONFIG_CHANGED_EVENT, slotId, 0)); + } else if (TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED.equals(action) + || TelephonyManager.ACTION_SIM_APPLICATION_STATE_CHANGED.equals(action)) { + int slotId = intent.getIntExtra(PhoneConstants.PHONE_KEY, -1); + int state = intent.getIntExtra( + TelephonyManager.EXTRA_SIM_STATE, TelephonyManager.SIM_STATE_UNKNOWN); + if (validateSlotId(slotId)) { + sendMessage(obtainMessage(ICC_CHANGED_EVENT, slotId, state)); + } + } else if (Intent.ACTION_USER_UNLOCKED.equals(action)) { + sendMessage(obtainMessage(USER_UNLOCKED_EVENT)); + } + } + }; + + public PinStorage(Context context) { + mContext = context; + mBootCount = getBootCount(); + mKeyStore = initializeKeyStore(); + mShortTermSecretKeyDurationMinutes = SHORT_TERM_KEY_DURATION_MINUTES; + + mIsDeviceSecure = isDeviceSecure(); + mIsDeviceLocked = mIsDeviceSecure ? isDeviceLocked() : false; + + // Register for necessary intents. + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); + intentFilter.addAction(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED); + intentFilter.addAction(TelephonyManager.ACTION_SIM_APPLICATION_STATE_CHANGED); + intentFilter.addAction(Intent.ACTION_USER_UNLOCKED); + mContext.registerReceiver(mCarrierConfigChangedReceiver, intentFilter); + + // Initialize the long term secret key. This needs to be present in all cases: + // - if the device is not secure or is locked: key does not require user authentication + // - if the device is secure and unlocked: key requires user authentication. + // The short term key is retrieved later when needed. + String alias = (!mIsDeviceSecure || mIsDeviceLocked) + ? KEYSTORE_ALIAS_LONG_TERM_ALWAYS : KEYSTORE_ALIAS_LONG_TERM_USER_AUTH; + mLongTermSecretKey = initializeSecretKey(alias, /*createIfAbsent=*/ true); + + // If the device is not securee or is unlocked, we can start logic. Otherwise we need to + // wait for the device to be unlocked and store any temporary PIN in RAM. + if (!mIsDeviceSecure || !mIsDeviceLocked) { + mRamStorage = null; + onDeviceReady(); + } else { + logd("Device is locked - Postponing initialization"); + mRamStorage = new SparseArray<>(); + } + } + + /** Store the {@code pin} for the {@code slotId}. */ + public synchronized void storePin(String pin, int slotId) { + String iccid = getIccid(slotId); + + if (!validatePin(pin) || !validateIccid(iccid) || !validateSlotId(slotId)) { + // We are unable to store the PIN. At least clear the old one, if present. + loge("storePin[%d] - Invalid PIN, slotId or ICCID", slotId); + clearPin(slotId); + return; + } + if (!isCacheAllowed(slotId)) { + logd("storePin[%d]: caching it not allowed", slotId); + return; + } + + logd("storePin[%d]", slotId); + + StoredPin storedPin = new StoredPin(); + storedPin.iccid = iccid; + storedPin.pin = pin; + storedPin.slotId = slotId; + storedPin.status = PinStatus.AVAILABLE; + + savePinInformation(slotId, storedPin); + } + + /** Clear the cached pin for the {@code slotId}. */ + public synchronized void clearPin(int slotId) { + logd("clearPin[%d]", slotId); + + if (!validateSlotId(slotId)) { + return; + } + savePinInformation(slotId, null); + } + + /** + * Return the cached pin for the {@code slotId}, or an empty string if it is not available. + * + * The method returns the PIN only if the state is VERIFICATION_READY. If the PIN is found, + * its state changes to AVAILABLE, so that it cannot be retrieved a second time during the + * same boot cycle. If the PIN verification fails, it will be removed after the failed attempt. + */ + public synchronized String getPin(int slotId) { + String iccid = getIccid(slotId); + if (!validateSlotId(slotId) || !validateIccid(iccid)) { + return ""; + } + + StoredPin storedPin = loadPinInformation(slotId); + if (storedPin != null) { + if (!storedPin.iccid.equals(iccid)) { + // The ICCID does not match: it's possible that the SIM card was changed. + // Delete the cached PIN. + savePinInformation(slotId, null); + } else if (storedPin.status == PinStatus.VERIFICATION_READY) { + logd("getPin[%d] - Found PIN ready for verification", slotId); + // Move the state to AVAILABLE, so that it cannot be retrieved again. + storedPin.status = PinStatus.AVAILABLE; + savePinInformation(slotId, storedPin); + return storedPin.pin; + } + } + return ""; + } + + /** + * Prepare for an unattended reboot. + * + * All PINs in AVAILABLE and VERIFICATION_READY state are moved to REBOOT_READY state. A + * timer is started to make sure that reboot occurs shortly after invoking this method. + * + * @return The result of the reboot preparation. + */ + @TelephonyManager.PrepareUnattendedRebootResult + public synchronized int prepareUnattendedReboot() { + // Unattended reboot should never occur before the device is unlocked. + if (mIsDeviceLocked) { + loge("prepareUnattendedReboot - Device is locked"); + return TelephonyManager.PREPARE_UNATTENDED_REBOOT_ERROR; + } + + // Start timer to make sure that device is rebooted shortly after this is executed. + if (!startTimer(TIMER_VALUE_BEFORE_OTA_MILLIS)) { + return TelephonyManager.PREPARE_UNATTENDED_REBOOT_ERROR; + } + + int numSlots = getSlotCount(); + SparseArray<StoredPin> storedPins = loadPinInformation(); + + // Delete any previous short-term key, if present: a new one is created (if needed). + deleteSecretKey(KEYSTORE_ALIAS_SHORT_TERM); + mShortTermSecretKey = null; + + // If any PIN is present, generate a new short-term key to save PIN(s) to + // non-volatile memory. + if (storedPins.size() > 0) { + mShortTermSecretKey = + initializeSecretKey(KEYSTORE_ALIAS_SHORT_TERM, /*createIfAbsent=*/ true); + } + + @TelephonyManager.PrepareUnattendedRebootResult + int result = TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS; + int storedCount = 0; + + for (int slotId = 0; slotId < numSlots; slotId++) { + StoredPin storedPin = storedPins.get(slotId); + if (storedPin != null) { + storedPin.status = PinStatus.REBOOT_READY; + if (!savePinInformation(slotId, storedPin)) { + result = TelephonyManager.PREPARE_UNATTENDED_REBOOT_ERROR; + break; + } + storedCount++; + } else if (isPinState(slotId, PINSTATE_ENABLED_VERIFIED)) { + // If PIN is not available, check if PIN will be required after reboot (current PIN + // status is enabled and verified). + loge("Slot %d requires PIN and is not cached", slotId); + result = TelephonyManager.PREPARE_UNATTENDED_REBOOT_PIN_REQUIRED; + } + } + + logd("prepareUnattendedReboot - Stored %d PINs", storedCount); + // Write metrics about number of stored PINs + TelephonyStatsLog.write(PIN_STORAGE_EVENT, + PIN_STORAGE_EVENT__EVENT__PIN_STORED_FOR_VERIFICATION, storedCount); + + return result; + } + + /** + * Execute logic when a secure device is unlocked. + * + * The temporary long-term key that does not require user verification is replaced by the long + * term key that requires user verification. The cached PIN temporarily stored in RAM are + * merged with those on disk from the previous boot. + */ + private synchronized void onUserUnlocked() { + if (!mIsDeviceLocked) { + // This should never happen. + // Nothing to do because the device was already unlocked before + return; + } + + logd("onUserUnlocked - Device is unlocked"); + + // It's possible that SIM PIN was already verified and stored temporarily in RAM. Load the + // data and erase the memory. + SparseArray<StoredPin> storedPinInRam = loadPinInformation(); + cleanRamStorage(); + + // Mark the device as unlocked + mIsDeviceLocked = false; + + // Replace the temporary long-term key without user authentication with a new long-term + // key that requires user authentication to save all PINs previously in RAM (all in + // AVAILABLE state) to disk. + mLongTermSecretKey = + initializeSecretKey(KEYSTORE_ALIAS_LONG_TERM_USER_AUTH, /*createIfAbsent=*/ true); + + // Save the PINs previously in RAM to disk, overwriting any PIN that might already exists. + for (int i = 0; i < storedPinInRam.size(); i++) { + savePinInformation(storedPinInRam.keyAt(i), storedPinInRam.valueAt(i)); + } + + // At this point the module is fully initialized. Execute the start logic. + onDeviceReady(); + + // Verify any pending PIN for SIM cards that need it. + verifyPendingPins(); + } + + /** + * Executes logic when module is fully ready. This occurs immediately if the device is not + * secure or after the user unlocks the device. + * + * At this point, the short-term key is initialized (if present), the configuration is read + * and the status of each PIN is updated as needed. + */ + private void onDeviceReady() { + logd("onDeviceReady"); + + // Try to initialize the short term key, if present, as this would be required to read + // stored PIN for verification. + mShortTermSecretKey = + initializeSecretKey(KEYSTORE_ALIAS_SHORT_TERM, /*createIfAbsent=*/ false); + + boolean otaReboot = false; + int slotCount = getSlotCount(); + for (int slotId = 0; slotId < slotCount; slotId++) { + // Read PIN information from storage + StoredPin storedPin = loadPinInformation(slotId); + if (storedPin == null) { + continue; + } + + // For each PIN in AVAILABLE state, check the boot count. + // If the boot count matches, it means that module crashed and it's ok to preserve + // the PIN code. If the boot count does not match, then delete those PINs. + if (storedPin.status == PinStatus.AVAILABLE) { + if (storedPin.bootCount != mBootCount) { + logd("Boot count [%d] does not match - remove PIN", slotId); + savePinInformation(slotId, null); + continue; + } + logd("Boot count [%d] matches - keep stored PIN", slotId); + } + + // If there is any PIN in REBOOT_READY state, move it to VERIFICATION_READY and start + // the timer. Don't change PINs that might be already in VERIFICATION_READY state + // (e.g. due to crash). + if (storedPin.status == PinStatus.REBOOT_READY) { + storedPin.status = PinStatus.VERIFICATION_READY; + savePinInformation(slotId, storedPin); + otaReboot = true; + } + } + if (otaReboot) { + startTimer(TIMER_VALUE_AFTER_OTA_MILLIS); + } + } + + /** + * Executes logic at the expiration of the timer. This method is common for two cases: + * - timer started after unattended reeboot to verify the SIM PIN automatically + * - timer started after prepareUnattendedReboot() is invoked. + */ + private synchronized void onTimerExpiration() { + logd("onTimerExpiration"); + + int discardedPin = 0; + int slotCount = getSlotCount(); + for (int slotId = 0; slotId < slotCount; slotId++) { + // Read PIN information from storage + StoredPin storedPin = loadPinInformation(slotId); + if (storedPin == null) { + continue; + } + + // Delete all PINs in VERIFICATION_READY state. This happens when reboot occurred after + // OTA, but the SIM card is not detected on the device. + if (storedPin.status == PinStatus.VERIFICATION_READY) { + logd("onTimerExpiration - Discarding PIN in slot %d", slotId); + savePinInformation(slotId, null); + discardedPin++; + continue; + } + + // Move all PINs in REBOOT_READY to AVAILABLE. This happens when + // prepareUnattendedReboot() is invoked, but the reboot does not occur. + if (storedPin.status == PinStatus.REBOOT_READY) { + logd("onTimerExpiration - Moving PIN in slot %d back to AVAILABLE", slotId); + storedPin.status = PinStatus.AVAILABLE; + savePinInformation(slotId, storedPin); + continue; + } + } + + // Delete short term key no matter the reason of the timer expiration. + // This is done after loading the PIN information, so that it's possible to change + // the status of the PIN as needed. + deleteSecretKey(KEYSTORE_ALIAS_SHORT_TERM); + mShortTermSecretKey = null; + + // Write metrics about number of discarded PINs + if (discardedPin > 0) { + TelephonyStatsLog.write(PIN_STORAGE_EVENT, + PIN_STORAGE_EVENT__EVENT__CACHED_PIN_DISCARDED, discardedPin); + } + } + + /** Handle the update of the {@code state} of the SIM card in {@code slotId}. */ + private synchronized void onSimStatusChange(int slotId, @SimState int state) { + logd("SIM card/application changed[%d]: %s", + slotId, SubscriptionInfoUpdater.simStateString(state)); + switch (state) { + case TelephonyManager.SIM_STATE_ABSENT: + case TelephonyManager.SIM_STATE_PIN_REQUIRED: { + // These states are likely to occur after a reboot, so we don't clear cached PINs + // in VERIFICATION_READY state, as they might be verified later, when the SIM is + // detected. On the other hand, we remove PINs in AVAILABLE state. + StoredPin storedPin = loadPinInformation(slotId); + if (storedPin != null && storedPin.status != PinStatus.VERIFICATION_READY) { + savePinInformation(slotId, null); + } + break; + } + case TelephonyManager.SIM_STATE_PUK_REQUIRED: + case TelephonyManager.SIM_STATE_PERM_DISABLED: + case TelephonyManager.SIM_STATE_CARD_IO_ERROR: + // These states indicate that the SIM card will need a manual PIN verification. + // Delete the cached PIN regardless of its state. + clearPin(slotId); + break; + case TelephonyManager.SIM_STATE_NETWORK_LOCKED: + case TelephonyManager.SIM_STATE_CARD_RESTRICTED: + case TelephonyManager.SIM_STATE_LOADED: + case TelephonyManager.SIM_STATE_READY: { + // These states can occur after successful PIN caching, so we don't clear cached + // PINs in AVAILABLE state, as they need to be retained. We clear any PIN in + // other states, as they are no longer needed for automatic verification. + StoredPin storedPin = loadPinInformation(slotId); + if (storedPin != null && storedPin.status != PinStatus.AVAILABLE) { + savePinInformation(slotId, null); + } + break; + } + + case TelephonyManager.SIM_STATE_NOT_READY: + case TelephonyManager.SIM_STATE_PRESENT: + default: + break; + } + } + + private void onCarrierConfigChanged(int slotId) { + logv("onCarrierConfigChanged[%d]", slotId); + if (!isCacheAllowed(slotId)) { + logd("onCarrierConfigChanged[%d] - PIN caching not allowed", slotId); + clearPin(slotId); + } + } + + private void onSupplyPinComplete(int slotId, boolean success) { + logd("onSupplyPinComplete[%d] - success: %s", slotId, success); + if (!success) { + // In case of failure to verify the PIN, delete the stored value. + // Otherwise nothing to do. + clearPin(slotId); + } + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case ICC_CHANGED_EVENT: + onSimStatusChange(/* slotId= */ msg.arg1, /* state= */ msg.arg2); + break; + case CARRIER_CONFIG_CHANGED_EVENT: + onCarrierConfigChanged(/* slotId= */ msg.arg1); + break; + case TIMER_EXPIRATION_EVENT: + onTimerExpiration(); + break; + case USER_UNLOCKED_EVENT: + onUserUnlocked(); + break; + case SUPPLY_PIN_COMPLETE: + AsyncResult ar = (AsyncResult) msg.obj; + boolean success = ar != null && ar.exception == null; + onSupplyPinComplete(/* slotId= */ msg.arg2, success); + break; + default: + // Nothing to do + break; + } + } + + /** Return if the device is secure (device PIN is enabled). */ + private boolean isDeviceSecure() { + KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class); + return keyguardManager != null ? keyguardManager.isDeviceSecure() : false; + } + + /** Return if the device is locked (device PIN is enabled and not verified). */ + private boolean isDeviceLocked() { + KeyguardManager keyguardManager = mContext.getSystemService(KeyguardManager.class); + return keyguardManager != null + ? keyguardManager.isDeviceSecure() && keyguardManager.isDeviceLocked() + : false; + } + + /** Loads the stored PIN informations for all SIM slots. */ + private SparseArray<StoredPin> loadPinInformation() { + SparseArray<StoredPin> result = new SparseArray<>(); + int slotCount = getSlotCount(); + for (int slotId = 0; slotId < slotCount; slotId++) { + StoredPin storedPin = loadPinInformation(slotId); + if (storedPin != null) { + result.put(slotId, storedPin); + } + } + return result; + } + + /** + * Loads the stored PIN information for the {@code slotId}. + * + * The RAM storage is used if the device is locked, the disk storage is used otherwise. + * This method tries to use both the long-term key and the short-term key (if available) + * to retrieve the PIN information, regardless of its status. + * + * @return the stored {@code StoredPin}, or null if not present. + */ + @Nullable + private StoredPin loadPinInformation(int slotId) { + if (!mLastCommitResult) { + // If the last commit failed, do not read from file, as we might retrieve stale data. + loge("Last commit failed - returning empty values"); + return null; + } + + StoredPin result = null; + + if (mIsDeviceLocked) { + // If the device is still locked, retrieve data from RAM storage. + if (mRamStorage != null && mRamStorage.get(slotId) != null) { + result = decryptStoredPin(mRamStorage.get(slotId), mLongTermSecretKey); + } + } else { + // Load both the stored PIN in available state (with long-term key) and in other states + // (with short-term key). At most one of them should be present at any given time and + // we treat the case wheere both are present as an error. + StoredPin availableStoredPin = loadPinInformationFromDisk( + slotId, SHARED_PREFS_AVAILABLE_PIN_BASE_KEY, mLongTermSecretKey); + StoredPin rebootStoredPin = loadPinInformationFromDisk( + slotId, SHARED_PREFS_REBOOT_PIN_BASE_KEY, mShortTermSecretKey); + if (availableStoredPin != null && rebootStoredPin == null) { + result = availableStoredPin; + } else if (availableStoredPin == null && rebootStoredPin != null) { + result = rebootStoredPin; + } + } + + // Validate the slot ID of the retrieved PIN information + if (result != null && result.slotId != slotId) { + loge("Load PIN: slot ID does not match (%d != %d)", result.slotId, slotId); + result = null; + } + + if (result != null) { + logv("Load PIN: %s", result.toString()); + } else { + logv("Load PIN for slot %d: null", slotId); + } + return result; + } + + /** + * Load the PIN information from a specific file in non-volatile memory. + * + * @param key the key in the {@code SharedPreferences} to read + * @param secretKey the key used for encryption/decryption + * @return the {@code StoredPin} from non-volatile memory. It returns a default instance in + * case of error. + */ + @Nullable + private StoredPin loadPinInformationFromDisk( + int slotId, String key, @Nullable SecretKey secretKey) { + String base64encryptedPin = + mContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + .getString(key + slotId, ""); + if (!base64encryptedPin.isEmpty()) { + try { + byte[] blob = Base64.decode(base64encryptedPin, Base64.DEFAULT); + return decryptStoredPin(blob, secretKey); + } catch (Exception e) { + // Nothing to do + } + } + return null; + } + + /** Load the PIN information from an encrypted binary blob. + * + * @param blob the encrypted binary blob + * @param secretKey the key used for encryption/decryption + * @return the decrypted {@code StoredPin}, or null in case of error. + */ + @Nullable + private StoredPin decryptStoredPin(byte[] blob, @Nullable SecretKey secretKey) { + if (secretKey != null) { + try { + byte[] decryptedPin = decrypt(secretKey, blob); + if (decryptedPin.length > 0) { + return StoredPin.parseFrom(decryptedPin); + } + } catch (Exception e) { + loge("cannot decrypt/parse PIN information", e); + } + } + return null; + } + + /** + * Stores the PIN information. + * + * If the device is locked, the PIN information is stored to RAM, othewrwise to disk. + * The PIN information is divided based on the PIN status and stored in two separate + * files in non-volatile memory, each encrypted with a different key. + * + * @param slotId the slot ID + * @param storedPin the PIN information to be stored + * @return true if the operation was successfully done, false otherwise. + */ + private boolean savePinInformation(int slotId, @Nullable StoredPin storedPin) { + // Populate the boot count + if (storedPin != null) { + storedPin.bootCount = mBootCount; + } + + // If the device is still locked, we can only save PINs in AVAILABLE state in RAM. + // NOTE: at this point, there should not be any PIN in any other state. + if (mIsDeviceLocked) { + return savePinInformationToRam(slotId, storedPin); + } + + // Remove any prvious key related to this slot. + SharedPreferences.Editor editor = + mContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .remove(SHARED_PREFS_AVAILABLE_PIN_BASE_KEY + slotId) + .remove(SHARED_PREFS_REBOOT_PIN_BASE_KEY + slotId); + + boolean result = true; + if (storedPin != null) { + // Available PINs are stored with a long-term key, while the PINs in other states + // are stored with a short-term key. + logd("Saving PIN for slot %d", slotId); + if (storedPin.status == PinStatus.AVAILABLE) { + result = savePinInformation(editor, slotId, storedPin, + SHARED_PREFS_AVAILABLE_PIN_BASE_KEY, mLongTermSecretKey); + } else { + result = savePinInformation(editor, slotId, storedPin, + SHARED_PREFS_REBOOT_PIN_BASE_KEY, mShortTermSecretKey); + } + } else { + logv("Deleting PIN for slot %d (if existed)", slotId); + } + + mLastCommitResult = result && editor.commit(); + return mLastCommitResult; + } + + /** + * Store the PIN information to a specific file in non-volatile memory. + * + * @param editor the {@code SharedPreferences.Editor} to use for storage + * @param slotId the slot ID + * @param storedPin the PIN information to store + * @param baseKey the base name of the key in the {@code SharedPreferences}. The full name is + * derived appending the value of {@code slotId}. + * @param secretKey the key used for encryption/decryption + * @return true if the operation was successful, false otherwise + */ + private boolean savePinInformation(SharedPreferences.Editor editor, int slotId, + StoredPin storedPin, String baseKey, SecretKey secretKey) { + if (secretKey == null) { + // Secret key for encryption is missing + return false; + } + if (slotId != storedPin.slotId) { + loge("Save PIN: the slotId does not match (%d != %d)", slotId, storedPin.slotId); + return false; + } + + logv("Save PIN: %s", storedPin.toString()); + + byte[] encryptedPin = encrypt(secretKey, StoredPin.toByteArray(storedPin)); + if (encryptedPin.length > 0) { + editor.putString( + baseKey + slotId, Base64.encodeToString(encryptedPin, Base64.DEFAULT)); + return true; + } else { + return false; + } + } + + /** Stored PIN information for slot {@code slotId} in RAM. */ + private boolean savePinInformationToRam(int slotId, @Nullable StoredPin storedPin) { + // Clear the RAM in all cases, to avoid leaking any previous PIN. + cleanRamStorage(slotId); + + if (storedPin == null) { + return true; + } + + if (storedPin.status == PinStatus.AVAILABLE) { + byte[] encryptedPin = encrypt(mLongTermSecretKey, StoredPin.toByteArray(storedPin)); + if (encryptedPin != null && encryptedPin.length > 0) { + logd("Saving PIN for slot %d in RAM", slotId); + mRamStorage.put(slotId, encryptedPin); + return true; + } + } + return false; + } + + + /** Erases all the PINs stored in RAM before a secure device is unlocked. */ + private void cleanRamStorage() { + int slotCount = getSlotCount(); + for (int slotId = 0; slotId < slotCount; slotId++) { + cleanRamStorage(slotId); + } + } + + /** Erases the PIN of slot {@code slotId} stored in RAM before a secure device is unlocked. */ + private void cleanRamStorage(int slotId) { + if (mRamStorage != null) { + byte[] data = mRamStorage.get(slotId); + if (data != null) { + Arrays.fill(data, (byte) 0); + } + mRamStorage.delete(slotId); + } + } + + /** + * Verifies all pending PIN codes that are ready for verification. + * + * The PIN verificartion is done if the PIN state is VERIFICATION_READY and the SIM + * card has the PIN enabled and not verified. + */ + private void verifyPendingPins() { + int slotCount = getSlotCount(); + for (int slotId = 0; slotId < slotCount; slotId++) { + if (isPinState(slotId, PINSTATE_ENABLED_NOT_VERIFIED)) { + verifyPendingPin(slotId); + } + } + } + + /** Verifies the PIN code for a given SIM card in slot {@code slotId}. */ + private void verifyPendingPin(int slotId) { + // We intentionally invoke getPin() here, as it updates the status and makes sure that + // same PIN is not used more than once + String pin = getPin(slotId); + if (pin.isEmpty()) { + // PIN is not available for verification: return. + return; + } + + logd("Perform automatic verification of PIN in slot %d", slotId); + + UiccProfile profile = UiccController.getInstance().getUiccProfileForPhone(slotId); + if (profile != null) { + Message onComplete = obtainMessage(SUPPLY_PIN_COMPLETE); + onComplete.arg2 = slotId; // arg1 is the number of remaining attempts in the response + profile.supplyPin(pin, onComplete); + } else { + logd("Perform automatic verification of PIN in slot %d not possible", slotId); + } + } + + /** Returns the boot count. */ + private int getBootCount() { + return Settings.Global.getInt( + mContext.getContentResolver(), + Settings.Global.BOOT_COUNT, + -1); + } + + /** Returns the number of available SIM slots. */ + private int getSlotCount() { + // Count the number of slots as the number of Phones. + // At power up, it is possible that number of phones is still unknown, so we query + // TelephonyManager for it. + try { + return PhoneFactory.getPhones().length; + } catch (Exception ex) { + return TelephonyManager.getDefault().getActiveModemCount(); + } + } + + private boolean startTimer(int duration) { + removeMessages(TIMER_EXPIRATION_EVENT); + return duration > 0 ? sendEmptyMessageDelayed(TIMER_EXPIRATION_EVENT, duration) : true; + } + + /** Returns the ICCID of the SIM card for the given {@code slotId}. */ + private String getIccid(int slotId) { + Phone phone = PhoneFactory.getPhone(slotId); + return phone != null ? phone.getFullIccSerialNumber() : ""; + } + + private boolean validatePin(String pin) { + return pin != null && pin.length() >= MIN_PIN_LENGTH && pin.length() <= MAX_PIN_LENGTH; + } + + private boolean validateIccid(String iccid) { + return iccid != null && iccid.length() >= MIN_ICCID_LENGTH; + } + + private boolean validateSlotId(int slotId) { + return slotId >= 0 && slotId < getSlotCount(); + } + + /** Checks if the PIN status of the SIM in slot {@code slotId} is a given {@code PinState}. */ + private boolean isPinState(int slotId, PinState pinState) { + UiccProfile profile = UiccController.getInstance().getUiccProfileForPhone(slotId); + if (profile != null) { + // Loop thru all possible app families to identify at least one that is available in + // order to check the PIN state. + int[] families = { + UiccController.APP_FAM_3GPP, + UiccController.APP_FAM_3GPP2, + UiccController.APP_FAM_IMS }; + for (int i = 0; i < families.length; i++) { + UiccCardApplication app = profile.getApplication(i); + if (app != null) { + return app.getPin1State() == pinState; + } + } + } + return false; + } + + /** Returns if the PIN cache is allowed for a given slot. */ + private boolean isCacheAllowed(int slotId) { + // Check overall device support + if (!mContext.getResources().getBoolean( + R.bool.config_allow_pin_storage_for_unattended_reboot)) { + logv("Pin caching disabled in resources"); + return false; + } + // Check carrier configuration + PersistableBundle config = null; + CarrierConfigManager configManager = + mContext.getSystemService(CarrierConfigManager.class); + if (configManager != null) { + Phone phone = PhoneFactory.getPhone(slotId); + if (phone != null) { + // If an invalid subId is used, this bundle will contain default values. + config = configManager.getConfigForSubId(phone.getSubId()); + } + } + if (config == null) { + config = CarrierConfigManager.getDefaultConfig(); + } + + return config.getBoolean( + CarrierConfigManager.KEY_STORE_SIM_PIN_FOR_UNATTENDED_REBOOT_BOOL, true); + } + + /** Initializes KeyStore and returns the instance. */ + @Nullable + private static KeyStore initializeKeyStore() { + KeyStore keyStore = null; + try { + keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER); + keyStore.load(/*param=*/ null); + } catch (Exception e) { + // Should never happen. + loge("Error loading KeyStore", e); + return null; + } + logv("KeyStore ready"); + return keyStore; + } + + /** + * Initializes a secret key and returns it. + * + * @param alias alias of the key in {@link KeyStore}. + * @param createIfAbsent indicates weather the key must be created if not already present. + * @return the {@link SecretKey}, or null if the key does not exist. + */ + @Nullable + private SecretKey initializeSecretKey(String alias, boolean createIfAbsent) { + if (mKeyStore == null) { + return null; + } + + SecretKey secretKey = getSecretKey(alias); + if (secretKey != null) { + logd("KeyStore: alias %s exists", alias); + return secretKey; + } else if (createIfAbsent) { + Date expiration = + KEYSTORE_ALIAS_SHORT_TERM.equals(alias) ? getShortLivedKeyValidityEnd() : null; + boolean isUserAuthRequired = + !KEYSTORE_ALIAS_LONG_TERM_ALWAYS.equals(alias) && isDeviceSecure(); + logd("KeyStore: alias %s does not exist - Creating (exp=%s, auth=%s)", + alias, expiration != null ? expiration.toString() : "", isUserAuthRequired); + return createSecretKey(alias, expiration, isUserAuthRequired); + } else { + // Nothing to do + logd("KeyStore: alias %s does not exist - Nothing to do", alias); + return null; + } + } + + /** + * Retrieves the secret key previously stored in {@link KeyStore}. + * + * @param alias alias of the key in {@link KeyStore}. + * @return the {@link SecretKey}, or null in case of error or if the key does not exist. + */ + @Nullable + private SecretKey getSecretKey(String alias) { + try { + final KeyStore.SecretKeyEntry secretKeyEntry = + (KeyStore.SecretKeyEntry) mKeyStore.getEntry(alias, null); + if (secretKeyEntry != null) { + return secretKeyEntry.getSecretKey(); + } + } catch (Exception e) { + // In case of exception, it means that key exists, but cannot be retrieved + // We delete the old key, so that a new key can be created. + loge("Exception with getting the key " + alias, e); + deleteSecretKey(alias); + } + return null; + } + + /** + * Generates a new secret key in {@link KeyStore}. + * + * @param alias alias of the key in {@link KeyStore}. + * @param expiration expiration of the key, or null if the key does not expire. + * @param isUserAuthRequired indicates if user authentication is required to use the key + * @return the created {@link SecretKey}, or null in case of error + */ + @Nullable + private SecretKey createSecretKey(String alias, Date expiration, boolean isUserAuthRequired) { + try { + final KeyGenerator keyGenerator = + KeyGenerator.getInstance(KEY_ALGORITHM_AES, ANDROID_KEY_STORE_PROVIDER); + KeyGenParameterSpec.Builder keyGenParameterSpec = + new KeyGenParameterSpec.Builder(alias, PURPOSE_ENCRYPT | PURPOSE_DECRYPT) + .setBlockModes(BLOCK_MODE_GCM) + .setEncryptionPaddings(ENCRYPTION_PADDING_NONE); + if (expiration != null) { + keyGenParameterSpec = keyGenParameterSpec + .setKeyValidityEnd(expiration); + } + if (isUserAuthRequired) { + keyGenParameterSpec = keyGenParameterSpec + .setUserAuthenticationRequired(true) + .setUserAuthenticationParameters(Integer.MAX_VALUE, AUTH_DEVICE_CREDENTIAL); + } + keyGenerator.init(keyGenParameterSpec.build()); + return keyGenerator.generateKey(); + } catch (Exception e) { + loge("Create key exception", e); + return null; + } + } + + /** Returns the validity end of a new short-lived key, or null if key does not expire. */ + @Nullable + private Date getShortLivedKeyValidityEnd() { + if (mShortTermSecretKeyDurationMinutes > 0) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date()); + calendar.add(Calendar.MINUTE, mShortTermSecretKeyDurationMinutes); + return calendar.getTime(); + } else { + return null; + } + } + + /** Deletes the short term key from KeyStore, if it exists. */ + private void deleteSecretKey(String alias) { + if (mKeyStore != null) { + logd("Delete key: %s", alias); + try { + mKeyStore.deleteEntry(alias); + } catch (Exception e) { + // Nothing to do. Even if the key removal fails, it becomes unusable. + loge("Delete key exception"); + } + } + } + + /** Returns the encrypted version of {@code input}, or an empty array in case of error. */ + private byte[] encrypt(SecretKey secretKey, byte[] input) { + if (secretKey == null) { + loge("Encrypt: Secret key is null"); + return new byte[0]; + } + + try { + final Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + + EncryptedPin encryptedPin = new EncryptedPin(); + encryptedPin.iv = cipher.getIV(); + encryptedPin.encryptedStoredPin = cipher.doFinal(input); + return EncryptedPin.toByteArray(encryptedPin); + } catch (Exception e) { + loge("Encrypt exception", e); + } + return new byte[0]; + } + + /** Returns the decrypted version of {@code input}, or an empty array in case of error. */ + private byte[] decrypt(SecretKey secretKey, byte[] input) { + if (secretKey == null) { + loge("Decrypt: Secret key is null"); + return new byte[0]; + } + + try { + EncryptedPin encryptedPin = EncryptedPin.parseFrom(input); + if (!ArrayUtils.isEmpty(encryptedPin.encryptedStoredPin) + && !ArrayUtils.isEmpty(encryptedPin.iv)) { + final Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORMATION); + final GCMParameterSpec spec = + new GCMParameterSpec(GCM_PARAMETER_TAG_BIT_LEN, encryptedPin.iv); + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); + return cipher.doFinal(encryptedPin.encryptedStoredPin); + } + } catch (Exception e) { + loge("Decrypt exception", e); + } + return new byte[0]; + } + + private static void logv(String format, Object... args) { + if (VDBG) { + Rlog.d(TAG, String.format(format, args)); + } + } + + private static void logd(String format, Object... args) { + Rlog.d(TAG, String.format(format, args)); + } + + private static void loge(String format, Object... args) { + Rlog.e(TAG, String.format(format, args)); + } + + private static void loge(String msg, Throwable tr) { + Rlog.e(TAG, msg, tr); + } + + void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("PinStorage:"); + pw.println(" mIsDeviceSecure=" + mIsDeviceSecure); + pw.println(" mIsDeviceLocked=" + mIsDeviceLocked); + pw.println(" isLongTermSecretKey=" + (boolean) (mLongTermSecretKey != null)); + pw.println(" isShortTermSecretKey=" + (boolean) (mShortTermSecretKey != null)); + if (VDBG) { + SparseArray<StoredPin> storedPins = loadPinInformation(); + for (int i = 0; i < storedPins.size(); i++) { + pw.println(" pin=" + storedPins.valueAt(i).toString()); + } + } + } +} diff --git a/src/java/com/android/internal/telephony/uicc/UiccController.java b/src/java/com/android/internal/telephony/uicc/UiccController.java index f8954a2ea0..23ff03c4b6 100644 --- a/src/java/com/android/internal/telephony/uicc/UiccController.java +++ b/src/java/com/android/internal/telephony/uicc/UiccController.java @@ -193,6 +193,9 @@ public class UiccController extends Handler { private UiccStateChangedLauncher mLauncher; private RadioConfig mRadioConfig; + /* The storage for the PIN codes. */ + private final PinStorage mPinStorage; + // LocalLog buffer to hold important SIM related events for debugging private static LocalLog sLocalLog = new LocalLog(TelephonyUtils.IS_DEBUGGABLE ? 250 : 100); @@ -254,6 +257,8 @@ public class UiccController extends Handler { PhoneConfigurationManager.registerForMultiSimConfigChange( this, EVENT_MULTI_SIM_CONFIG_CHANGED, null); + + mPinStorage = new PinStorage(mContext); } /** @@ -887,6 +892,11 @@ public class UiccController extends Handler { return mDefaultEuiccCardId; } + /** Get the {@link PinStorage}. */ + public PinStorage getPinStorage() { + return mPinStorage; + } + private ArrayList<String> loadCardStrings() { String cardStrings = PreferenceManager.getDefaultSharedPreferences(mContext).getString(CARD_STRINGS, ""); @@ -1305,5 +1315,6 @@ public class UiccController extends Handler { } pw.println(" sLocalLog= "); sLocalLog.dump(fd, pw, args); + mPinStorage.dump(fd, pw, args); } } diff --git a/src/java/com/android/internal/telephony/uicc/UiccProfile.java b/src/java/com/android/internal/telephony/uicc/UiccProfile.java index 37097dbe15..bd0a121e86 100644 --- a/src/java/com/android/internal/telephony/uicc/UiccProfile.java +++ b/src/java/com/android/internal/telephony/uicc/UiccProfile.java @@ -16,6 +16,10 @@ package com.android.internal.telephony.uicc; +import static com.android.internal.telephony.TelephonyStatsLog.PIN_STORAGE_EVENT; +import static com.android.internal.telephony.TelephonyStatsLog.PIN_STORAGE_EVENT__EVENT__PIN_VERIFICATION_FAILURE; +import static com.android.internal.telephony.TelephonyStatsLog.PIN_STORAGE_EVENT__EVENT__PIN_VERIFICATION_SUCCESS; + import android.annotation.Nullable; import android.app.ActivityManager; import android.app.usage.UsageStatsManager; @@ -59,6 +63,7 @@ import com.android.internal.telephony.Phone; import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.PhoneFactory; import com.android.internal.telephony.SubscriptionController; +import com.android.internal.telephony.TelephonyStatsLog; import com.android.internal.telephony.cat.CatService; import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType; import com.android.internal.telephony.uicc.IccCardApplicationStatus.PersoSubState; @@ -133,6 +138,7 @@ public class UiccProfile extends IccCard { private static final int EVENT_CARRIER_PRIVILEGES_LOADED = 13; private static final int EVENT_CARRIER_CONFIG_CHANGED = 14; private static final int EVENT_CARRIER_PRIVILEGES_TEST_OVERRIDE_SET = 15; + private static final int EVENT_SUPPLY_ICC_PIN_DONE = 16; // NOTE: any new EVENT_* values must be added to eventToString. private TelephonyManager mTelephonyManager; @@ -247,7 +253,7 @@ public class UiccProfile extends IccCard { case EVENT_CLOSE_LOGICAL_CHANNEL_DONE: case EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE: case EVENT_TRANSMIT_APDU_BASIC_CHANNEL_DONE: - case EVENT_SIM_IO_DONE: + case EVENT_SIM_IO_DONE: { AsyncResult ar = (AsyncResult) msg.obj; if (ar.exception != null) { logWithLocalLog("handleMessage: Error in SIM access with exception " @@ -256,6 +262,7 @@ public class UiccProfile extends IccCard { AsyncResult.forMessage((Message) ar.userObj, ar.result, ar.exception); ((Message) ar.userObj).sendToTarget(); break; + } case EVENT_CARRIER_PRIVILEGES_TEST_OVERRIDE_SET: if (msg.obj == null) { @@ -267,6 +274,28 @@ public class UiccProfile extends IccCard { refresh(); break; + case EVENT_SUPPLY_ICC_PIN_DONE: { + AsyncResult ar = (AsyncResult) msg.obj; + if (ar.exception != null) { + // An error occurred during automatic PIN verification. At this point, + // clear the cache and propagate the state. + loge("An error occurred during internal PIN verification"); + UiccController.getInstance().getPinStorage().clearPin(mPhoneId); + updateExternalState(); + } else { + log("Internal PIN verification was successful!"); + // Nothing to do. + } + // Update metrics: + TelephonyStatsLog.write( + PIN_STORAGE_EVENT, + ar.exception != null + ? PIN_STORAGE_EVENT__EVENT__PIN_VERIFICATION_FAILURE + : PIN_STORAGE_EVENT__EVENT__PIN_VERIFICATION_SUCCESS, + /* number_of_pins= */ 1); + break; + } + default: loge("handleMessage: Unhandled message with number: " + msg.what); break; @@ -593,6 +622,17 @@ public class UiccProfile extends IccCard { log("updateExternalState: card locked and records loaded; " + "setting state to locked"); } + // If the PIN code is required and an available cached PIN is available, intercept + // the update of external state and perform an internal PIN verification. + if (lockedState == IccCardConstants.State.PIN_REQUIRED) { + String pin = UiccController.getInstance().getPinStorage().getPin(mPhoneId); + if (!pin.isEmpty()) { + log("PIN_REQUIRED[" + mPhoneId + "] - Cache present"); + mCi.supplyIccPin(pin, mHandler.obtainMessage(EVENT_SUPPLY_ICC_PIN_DONE)); + return; + } + } + setExternalState(lockedState); } else { if (VDBG) { @@ -1804,6 +1844,7 @@ public class UiccProfile extends IccCard { case EVENT_CARRIER_CONFIG_CHANGED: return "CARRIER_CONFIG_CHANGED"; case EVENT_CARRIER_PRIVILEGES_TEST_OVERRIDE_SET: return "CARRIER_PRIVILEGES_TEST_OVERRIDE_SET"; + case EVENT_SUPPLY_ICC_PIN_DONE: return "SUPPLY_ICC_PIN_DONE"; default: return "UNKNOWN(" + event + ")"; } } diff --git a/tests/telephonytests/src/com/android/internal/telephony/ContextFixture.java b/tests/telephonytests/src/com/android/internal/telephony/ContextFixture.java index 9cef950158..150ac61006 100644 --- a/tests/telephonytests/src/com/android/internal/telephony/ContextFixture.java +++ b/tests/telephonytests/src/com/android/internal/telephony/ContextFixture.java @@ -31,6 +31,7 @@ import android.app.ActivityManager; import android.app.AlarmManager; import android.app.AppOpsManager; import android.app.DownloadManager; +import android.app.KeyguardManager; import android.app.NotificationManager; import android.app.usage.UsageStatsManager; import android.content.BroadcastReceiver; @@ -261,6 +262,8 @@ public class ContextFixture implements TestFixture<Context> { return mTelephonyRegistryManager; case Context.SYSTEM_CONFIG_SERVICE: return mSystemConfigManager; + case Context.KEYGUARD_SERVICE: + return mKeyguardManager; case Context.VCN_MANAGEMENT_SERVICE: return mVcnManager; case Context.BATTERY_STATS_SERVICE: @@ -299,6 +302,8 @@ public class ContextFixture implements TestFixture<Context> { return Context.ACTIVITY_SERVICE; } else if (serviceClass == TelephonyManager.class) { return Context.TELEPHONY_SERVICE; + } else if (serviceClass == KeyguardManager.class) { + return Context.KEYGUARD_SERVICE; } else if (serviceClass == VcnManager.class) { return Context.VCN_MANAGEMENT_SERVICE; } @@ -642,6 +647,7 @@ public class ContextFixture implements TestFixture<Context> { mock(TelephonyRegistryManager.class); private final SystemConfigManager mSystemConfigManager = mock(SystemConfigManager.class); private final PowerWhitelistManager mPowerWhitelistManager = mock(PowerWhitelistManager.class); + private final KeyguardManager mKeyguardManager = mock(KeyguardManager.class); private final VcnManager mVcnManager = mock(VcnManager.class); private final ContentProvider mContentProvider = spy(new FakeContentProvider()); diff --git a/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java b/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java index 1b364a48e4..f83512c44b 100644 --- a/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java +++ b/tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.eq; import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.IActivityManager; +import android.app.KeyguardManager; import android.app.usage.NetworkStatsManager; import android.content.ContentResolver; import android.content.Context; @@ -101,6 +102,7 @@ import com.android.internal.telephony.test.SimulatedCommandsVerifier; import com.android.internal.telephony.uicc.IccCardStatus; import com.android.internal.telephony.uicc.IccRecords; import com.android.internal.telephony.uicc.IsimUiccRecords; +import com.android.internal.telephony.uicc.PinStorage; import com.android.internal.telephony.uicc.RuimRecords; import com.android.internal.telephony.uicc.SIMRecords; import com.android.internal.telephony.uicc.UiccCard; @@ -316,6 +318,8 @@ public abstract class TelephonyTest { protected WifiInfo mWifiInfo; @Mock protected ImsStats mImsStats; + @Mock + protected PinStorage mPinStorage; protected ActivityManager mActivityManager; protected ImsCallProfile mImsCallProfile; @@ -328,6 +332,7 @@ public abstract class TelephonyTest { protected AppOpsManager mAppOpsManager; protected CarrierConfigManager mCarrierConfigManager; protected UserManager mUserManager; + protected KeyguardManager mKeyguardManager; protected VcnManager mVcnManager; protected SimulatedCommands mSimulatedCommands; protected ContextFixture mContextFixture; @@ -469,6 +474,7 @@ public abstract class TelephonyTest { mCarrierConfigManager = (CarrierConfigManager) mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE); mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); mVcnManager = mContext.getSystemService(VcnManager.class); //mTelephonyComponentFactory @@ -577,6 +583,7 @@ public abstract class TelephonyTest { } }).when(mUiccController).getIccRecords(anyInt(), anyInt()); doReturn(new UiccSlot[] {}).when(mUiccController).getUiccSlots(); + doReturn(mPinStorage).when(mUiccController).getPinStorage(); //UiccCardApplication doReturn(mSimRecords).when(mUiccCardApplication3gpp).getIccRecords(); diff --git a/tests/telephonytests/src/com/android/internal/telephony/uicc/PinStorageTest.java b/tests/telephonytests/src/com/android/internal/telephony/uicc/PinStorageTest.java new file mode 100644 index 0000000000..f165a9ef4d --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/uicc/PinStorageTest.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2020 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 static com.android.internal.telephony.uicc.IccCardStatus.PinState.PINSTATE_ENABLED_VERIFIED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import android.content.Intent; +import android.os.PersistableBundle; +import android.preference.PreferenceManager; +import android.provider.Settings; +import android.telephony.CarrierConfigManager; +import android.telephony.TelephonyManager; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; + +import com.android.internal.R; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.TelephonyTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class PinStorageTest extends TelephonyTest { + private static final String ICCID_1 = "89010003006562472370"; + private static final String ICCID_2 = "89010003006562472399"; + private static final String ICCID_INVALID = "1234"; + + private int mBootCount; + private int mSimulatedRebootsCount; + private PinStorage mPinStorage; + + private void simulateReboot() { + mSimulatedRebootsCount++; + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.BOOT_COUNT, mBootCount + mSimulatedRebootsCount); + + mPinStorage = new PinStorage(mContext); + mPinStorage.mShortTermSecretKeyDurationMinutes = 0; + } + + @Before + public void setUp() throws Exception { + super.setUp(this.getClass().getSimpleName()); + + // Store boot count, so that correct value can be restored at the end. + mBootCount = Settings.Global.getInt( + mContext.getContentResolver(), Settings.Global.BOOT_COUNT, -1); + mSimulatedRebootsCount = 0; + + // Clear shared preferences. + PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getContext()) + .edit().clear().commit(); + // Enable PIN storage in resources + mContextFixture.putBooleanResource( + R.bool.config_allow_pin_storage_for_unattended_reboot, true); + // Remaining setup + doReturn(ICCID_1).when(mPhone).getFullIccSerialNumber(); + // Simulate the device is not secure by default + when(mKeyguardManager.isDeviceSecure()).thenReturn(false); + when(mKeyguardManager.isDeviceLocked()).thenReturn(false); + + mPinStorage = new PinStorage(mContext); + mPinStorage.mShortTermSecretKeyDurationMinutes = 0; + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + + // Restore boot count + if (mBootCount == -1) { + Settings.Global.resetToDefaults( + mContext.getContentResolver(), Settings.Global.BOOT_COUNT); + } else { + Settings.Global.putInt( + mContext.getContentResolver(), Settings.Global.BOOT_COUNT, mBootCount); + } + } + + @Test + @SmallTest + public void storePin_withoutReboot_pinCannotBeRetrieved() { + mPinStorage.storePin("1234", 0); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_normalReboot_pinCannotBeRetrieved() { + mPinStorage.storePin("1234", 0); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_crash_pinCannotBeRetrieved() { + mPinStorage.storePin("1234", 0); + + // Simulate crash + mPinStorage = new PinStorage(mContext); + mPinStorage.mShortTermSecretKeyDurationMinutes = 0; + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_unattendedReboot_pinCanBeRetrievedOnce() { + mPinStorage.storePin("1234", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + // PIN can be retrieved only once after unattended reboot + assertThat(mPinStorage.getPin(0)).isEqualTo("1234"); + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_unattendedReboot_deviceIsLocked() { + // Simulate the device is still locked + when(mKeyguardManager.isDeviceSecure()).thenReturn(true); + when(mKeyguardManager.isDeviceLocked()).thenReturn(true); + simulateReboot(); + + mPinStorage.storePin("1234", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_ERROR); + + simulateReboot(); + + // PIN cannot be retrieved + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_unattendedReboot_pinIsRemovedAfterDelay() { + mPinStorage.storePin("1234", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + // Move time forward by 60 seconds + moveTimeForward(60000); + processAllMessages(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + + // Simulate a second unattended reboot to make sure that PIN was deleted. + result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_unattendedRebootNotDone_pinCannotBeRetrieved() { + mPinStorage.storePin("1234", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + // Move time forward by 60 seconds before simulating reboot + moveTimeForward(60000); + processAllMessages(); + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_unattendedReboot_iccidChange() { + mPinStorage.storePin("1234", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + // Switch to a different ICCID in the device after the reboot + doReturn(ICCID_2).when(mPhone).getFullIccSerialNumber(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + + // Switch back to the initial ICCID to make sure that PIN was deleted. + doReturn(ICCID_1).when(mPhone).getFullIccSerialNumber(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void clearPin_pinCannotBeRetrieved() { + mPinStorage.storePin("1234", 0); + mPinStorage.clearPin(0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_pinChanged_pinIsUpdated() { + mPinStorage.storePin("1234", 0); + mPinStorage.storePin("5678", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo("5678"); + } + + @Test + @SmallTest + public void storePin_pinTooShort_pinIsNotStored() { + mPinStorage.storePin("12", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_pinTooLong_pinIsNotStored() { + mPinStorage.storePin("123456789", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_invalidIccid_pinIsNotStored() { + doReturn(ICCID_INVALID).when(mPhone).getFullIccSerialNumber(); + + mPinStorage.storePin("1234", 0); + int result = mPinStorage.prepareUnattendedReboot(); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_disabledInResources_pinIsNotStored() { + mContextFixture.putBooleanResource( + R.bool.config_allow_pin_storage_for_unattended_reboot, false); + + mPinStorage.storePin("1234", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_disabledInResources_containsSimWithPinEnabledAndVerified() { + mContextFixture.putBooleanResource( + R.bool.config_allow_pin_storage_for_unattended_reboot, false); + + when(mUiccController.getUiccProfileForPhone(anyInt())).thenReturn(mUiccProfile); + when(mUiccCardApplication3gpp.getPin1State()).thenReturn(PINSTATE_ENABLED_VERIFIED); + + mPinStorage.storePin("1234", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_PIN_REQUIRED); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_disabledInCarrierConfig_pinIsNotStored() { + PersistableBundle carrierConfigs = new PersistableBundle(); + carrierConfigs.putBoolean( + CarrierConfigManager.KEY_STORE_SIM_PIN_FOR_UNATTENDED_REBOOT_BOOL, false); + when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(carrierConfigs); + + mPinStorage.storePin("1234", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_changeToDisabledInCarrierConfig_pinIsRemoved() { + mPinStorage.storePin("1234", 0); + + // Simulate change in the carrier configuration + PersistableBundle carrierConfigs = new PersistableBundle(); + carrierConfigs.putBoolean( + CarrierConfigManager.KEY_STORE_SIM_PIN_FOR_UNATTENDED_REBOOT_BOOL, false); + when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(carrierConfigs); + final Intent intent = new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); + intent.putExtra(CarrierConfigManager.EXTRA_SLOT_INDEX, 0); + mContext.sendBroadcast(intent); + processAllMessages(); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_simIsRemoved_pinIsRemoved() { + mPinStorage.storePin("1234", 0); + + // SIM is removed + final Intent intent = new Intent(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED); + intent.putExtra(PhoneConstants.PHONE_KEY, 0); + intent.putExtra(TelephonyManager.EXTRA_SIM_STATE, TelephonyManager.SIM_STATE_ABSENT); + mContext.sendBroadcast(intent); + processAllMessages(); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } + + @Test + @SmallTest + public void storePin_simReadyAfterUnattendedReboot_pinIsRemoved() { + mPinStorage.storePin("1234", 0); + + int result = mPinStorage.prepareUnattendedReboot(); + assertThat(result).isEqualTo(TelephonyManager.PREPARE_UNATTENDED_REBOOT_SUCCESS); + + simulateReboot(); + + // SIM is fully loaded before cached PIN is used. + final Intent intent = new Intent(TelephonyManager.ACTION_SIM_APPLICATION_STATE_CHANGED); + intent.putExtra(PhoneConstants.PHONE_KEY, 0); + intent.putExtra(TelephonyManager.EXTRA_SIM_STATE, TelephonyManager.SIM_STATE_LOADED); + mContext.sendBroadcast(intent); + processAllMessages(); + + assertThat(mPinStorage.getPin(0)).isEqualTo(""); + } +} |