aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTreehugger Robot <treehugger-gerrit@google.com>2021-02-24 17:45:27 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2021-02-24 17:45:27 +0000
commit1e7b0e68f46e181d1982ccbc32b9177fc618e518 (patch)
tree1a74744acf9d4678bc5181e37dfe16246d08d7a0
parentf2fa93cfe74302592a5aa6db3965c4c8be333074 (diff)
parentc047965e68acbfaaf84e4592a76ab8508bbdb5fd (diff)
downloadtelephony-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
-rw-r--r--proto/src/pin_storage.proto62
-rw-r--r--src/java/com/android/internal/telephony/uicc/PinStorage.java1149
-rw-r--r--src/java/com/android/internal/telephony/uicc/UiccController.java11
-rw-r--r--src/java/com/android/internal/telephony/uicc/UiccProfile.java43
-rw-r--r--tests/telephonytests/src/com/android/internal/telephony/ContextFixture.java6
-rw-r--r--tests/telephonytests/src/com/android/internal/telephony/TelephonyTest.java7
-rw-r--r--tests/telephonytests/src/com/android/internal/telephony/uicc/PinStorageTest.java414
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("");
+ }
+}