diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java index 1bb6df0aaf6..f4cfabc754c 100644 --- a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java +++ b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java @@ -17,6 +17,7 @@ // TODO (b/35202196): move this class out of the root of the package. package com.android.settings.password; +import static android.app.Activity.RESULT_FIRST_USER; import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_LOCK_ATTEMPTS_FAILED; import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; @@ -24,7 +25,6 @@ import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME; import android.annotation.Nullable; import android.app.Dialog; import android.app.KeyguardManager; -import android.app.RemoteLockscreenValidationResult; import android.app.RemoteLockscreenValidationSession; import android.app.admin.DevicePolicyManager; import android.app.admin.ManagedSubscriptionsPolicy; @@ -38,7 +38,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; -import android.service.remotelockscreenvalidation.IRemoteLockscreenValidationCallback; import android.service.remotelockscreenvalidation.RemoteLockscreenValidationClient; import android.telecom.TelecomManager; import android.text.TextUtils; @@ -55,14 +54,11 @@ import androidx.fragment.app.FragmentManager; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockscreenCredential; -import com.android.security.SecureBox; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.InstrumentedFragment; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; +import com.google.android.setupdesign.GlifLayout; /** * Base fragment to be shared for PIN/Pattern/Password confirmation fragments. @@ -89,9 +85,13 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr /** Time we wait before clearing a wrong input attempt (e.g. pattern) and the error message. */ protected static final long CLEAR_WRONG_ATTEMPT_TIMEOUT_MS = 3000; + protected static final String FRAGMENT_TAG_REMOTE_LOCKSCREEN_VALIDATION = + "remote_lockscreen_validation"; + protected boolean mReturnCredentials = false; protected boolean mReturnGatekeeperPassword = false; protected boolean mForceVerifyPath = false; + protected GlifLayout mGlifLayout; protected CheckBox mCheckBox; protected Button mCancelButton; /** Button allowing managed profile password reset, null when is not shown. */ @@ -109,8 +109,8 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr protected BiometricManager mBiometricManager; @Nullable protected RemoteLockscreenValidationSession mRemoteLockscreenValidationSession; /** Credential saved so the credential can be set for device if remote validation passes */ - @Nullable protected LockscreenCredential mDeviceCredentialGuess; @Nullable protected RemoteLockscreenValidationClient mRemoteLockscreenValidationClient; + protected RemoteLockscreenValidationFragment mRemoteLockscreenValidationFragment; private boolean isInternalActivity() { return (getActivity() instanceof ConfirmLockPassword.InternalActivity) @@ -136,8 +136,8 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr FeatureFlagUtils.SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION)) { mRemoteValidation = true; } else { - Log.e(TAG, "Remote device credential validation not enabled."); - getActivity().finish(); + onRemoteLockscreenValidationFailure( + "Remote lockscreen validation not enabled."); } } if (mRemoteValidation) { @@ -146,23 +146,31 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr RemoteLockscreenValidationSession.class); if (mRemoteLockscreenValidationSession == null || mRemoteLockscreenValidationSession.getRemainingAttempts() == 0) { - Log.e(TAG, "RemoteLockscreenValidationSession is null or " + onRemoteLockscreenValidationFailure("RemoteLockscreenValidationSession is null or " + "no more attempts for remote lockscreen validation."); - getActivity().finish(); } ComponentName remoteLockscreenValidationServiceComponent = intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME, ComponentName.class); if (remoteLockscreenValidationServiceComponent == null) { - Log.e(TAG, "RemoteLockscreenValidationService ComponentName is null"); - getActivity().finish(); + onRemoteLockscreenValidationFailure( + "RemoteLockscreenValidationService ComponentName is null"); } mRemoteLockscreenValidationClient = RemoteLockscreenValidationClient .create(getContext(), remoteLockscreenValidationServiceComponent); if (!mRemoteLockscreenValidationClient.isServiceAvailable()) { - Log.e(TAG, String.format("RemoteLockscreenValidationService at %s is not available", + onRemoteLockscreenValidationFailure(String.format( + "RemoteLockscreenValidationService at %s is not available", remoteLockscreenValidationServiceComponent.getClassName())); - getActivity().finish(); + } + + mRemoteLockscreenValidationFragment = + (RemoteLockscreenValidationFragment) getFragmentManager() + .findFragmentByTag(FRAGMENT_TAG_REMOTE_LOCKSCREEN_VALIDATION); + if (mRemoteLockscreenValidationFragment == null) { + mRemoteLockscreenValidationFragment = new RemoteLockscreenValidationFragment(); + getFragmentManager().beginTransaction().add(mRemoteLockscreenValidationFragment, + FRAGMENT_TAG_REMOTE_LOCKSCREEN_VALIDATION).commit(); } } @@ -194,8 +202,10 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr mCancelButton.setOnClickListener(v -> { if (hasAlternateButton) { getActivity().setResult(KeyguardManager.RESULT_ALTERNATE); + getActivity().finish(); + } else if (mRemoteValidation) { + onRemoteLockscreenValidationFailure("Forgot lockscreen credential button pressed."); } - getActivity().finish(); }); setupForgotButtonIfManagedProfile(view); @@ -299,17 +309,11 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr if (mRemoteLockscreenValidationClient != null) { mRemoteLockscreenValidationClient.disconnect(); } - if (mDeviceCredentialGuess != null) { - mDeviceCredentialGuess.zeroize(); - } super.onDestroy(); } protected abstract void authenticationSucceeded(); - protected abstract void onRemoteDeviceCredentialValidationResult( - RemoteLockscreenValidationResult result); - public void prepareEnterAnimation() { } @@ -411,43 +415,33 @@ public abstract class ConfirmDeviceCredentialBaseFragment extends InstrumentedFr } protected void validateGuess(LockscreenCredential credentialGuess) { - if (mCheckBox.isChecked()) { - // Keep credential in memory since user wants to set guess as screen lock. - mDeviceCredentialGuess = credentialGuess; - } else if (mDeviceCredentialGuess != null) { - mDeviceCredentialGuess.zeroize(); - } - - mRemoteLockscreenValidationClient.validateLockscreenGuess( - encryptDeviceCredentialGuess(credentialGuess.getCredential()), - new IRemoteLockscreenValidationCallback.Stub() { - @Override - public void onSuccess(RemoteLockscreenValidationResult result) { - mHandler.post(()->onRemoteDeviceCredentialValidationResult(result)); - } - - @Override - public void onFailure(String message) { - Log.e(TAG, "A failure occurred while trying " - + "to validate lockscreen guess: " + message); - mHandler.post(()->getActivity().finish()); - } - }); + mRemoteLockscreenValidationFragment.validateLockscreenGuess( + mRemoteLockscreenValidationClient, credentialGuess, + mRemoteLockscreenValidationSession.getSourcePublicKey(), mCheckBox.isChecked()); } - private byte[] encryptDeviceCredentialGuess(byte[] guess) { - try { - byte[] encodedPublicKey = mRemoteLockscreenValidationSession.getSourcePublicKey(); - PublicKey publicKey = SecureBox.decodePublicKey(encodedPublicKey); - return SecureBox.encrypt( - publicKey, - /* sharedSecret= */ null, - LockPatternUtils.ENCRYPTED_REMOTE_CREDENTIALS_HEADER, - guess); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - Log.w(TAG, "Error encrypting device credential guess. Returning empty byte[].", e); - return new byte[0]; + protected void updateRemoteLockscreenValidationViews() { + if (!mRemoteValidation || mRemoteLockscreenValidationFragment == null) { + return; } + + boolean enable = mRemoteLockscreenValidationFragment.isRemoteValidationInProgress(); + mGlifLayout.setProgressBarShown(enable); + mCheckBox.setEnabled(!enable); + mCancelButton.setEnabled(!enable); + } + + /** + * Finishes the activity with result code {@link android.app.Activity#RESULT_FIRST_USER} + * after logging the error message. + * @param message Optional message to log. + */ + public void onRemoteLockscreenValidationFailure(String message) { + if (!TextUtils.isEmpty(message)) { + Log.w(TAG, message); + } + getActivity().setResult(RESULT_FIRST_USER); + getActivity().finish(); } protected abstract void onShowError(); diff --git a/src/com/android/settings/password/ConfirmLockPassword.java b/src/com/android/settings/password/ConfirmLockPassword.java index 81bd8c231a6..03b89f25051 100644 --- a/src/com/android/settings/password/ConfirmLockPassword.java +++ b/src/com/android/settings/password/ConfirmLockPassword.java @@ -71,8 +71,6 @@ import com.android.settings.R; import com.android.settingslib.animation.AppearAnimationUtils; import com.android.settingslib.animation.DisappearAnimationUtils; -import com.google.android.setupdesign.GlifLayout; - import java.util.ArrayList; public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { @@ -127,7 +125,8 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { public static class ConfirmLockPasswordFragment extends ConfirmDeviceCredentialBaseFragment implements OnClickListener, OnEditorActionListener, - CredentialCheckResultTracker.Listener, SaveChosenLockWorkerBase.Listener { + CredentialCheckResultTracker.Listener, SaveChosenLockWorkerBase.Listener, + RemoteLockscreenValidationFragment.Listener { private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result"; private ImeAwareEditText mPasswordEntry; private TextViewInputDisabler mPasswordEntryInputDisabler; @@ -140,7 +139,6 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { private AppearAnimationUtils mAppearAnimationUtils; private DisappearAnimationUtils mDisappearAnimationUtils; private boolean mIsManagedProfile; - private GlifLayout mGlifLayout; private CharSequence mCheckBoxLabel; // required constructor for fragments @@ -255,6 +253,7 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { ? R.string.lockpassword_forgot_password : R.string.lockpassword_forgot_pin); } + updateRemoteLockscreenValidationViews(); } if (mForgotButton != null) { @@ -405,6 +404,9 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { mCountdownTimer = null; } mCredentialCheckResultTracker.setListener(null); + if (mRemoteLockscreenValidationFragment != null) { + mRemoteLockscreenValidationFragment.setListener(null, /* handler= */ null); + } } @Override @@ -426,6 +428,9 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { mLockPatternUtils.getCurrentFailedPasswordAttempts(mEffectiveUserId)); } mCredentialCheckResultTracker.setListener(this); + if (mRemoteLockscreenValidationFragment != null) { + mRemoteLockscreenValidationFragment.setListener(this, mHandler); + } } @Override @@ -436,13 +441,17 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { private void updatePasswordEntry() { final boolean isLockedOut = mLockPatternUtils.getLockoutAttemptDeadline(mEffectiveUserId) != 0; - mPasswordEntry.setEnabled(!isLockedOut); - mPasswordEntryInputDisabler.setInputEnabled(!isLockedOut); - if (isLockedOut) { - mImm.hideSoftInputFromWindow(mPasswordEntry.getWindowToken(), 0 /*flags*/); - } else { + final boolean isRemoteLockscreenValidationInProgress = + mRemoteLockscreenValidationFragment != null + && mRemoteLockscreenValidationFragment.isRemoteValidationInProgress(); + boolean shouldEnableInput = !isLockedOut && !isRemoteLockscreenValidationInProgress; + mPasswordEntry.setEnabled(shouldEnableInput); + mPasswordEntryInputDisabler.setInputEnabled(shouldEnableInput); + if (shouldEnableInput) { mPasswordEntry.scheduleShowSoftInput(); mPasswordEntry.requestFocus(); + } else { + mImm.hideSoftInputFromWindow(mPasswordEntry.getWindowToken(), /* flags= */0); } } @@ -472,7 +481,8 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { if (mRemoteValidation) { validateGuess(credential); - mGlifLayout.setProgressBarShown(true); + updateRemoteLockscreenValidationViews(); + updatePasswordEntry(); return; } @@ -604,14 +614,15 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { } @Override - protected void onRemoteDeviceCredentialValidationResult( + public void onRemoteLockscreenValidationResult( RemoteLockscreenValidationResult result) { switch (result.getResultCode()) { case RemoteLockscreenValidationResult.RESULT_GUESS_VALID: - if (mCheckBox.isChecked()) { + if (mCheckBox.isChecked() && mRemoteLockscreenValidationFragment + .getLockscreenCredential() != null) { + Log.i(TAG, "Setting device screen lock to the other device's screen lock."); ChooseLockPassword.SaveAndFinishWorker saveAndFinishWorker = new ChooseLockPassword.SaveAndFinishWorker(); - Log.i(TAG, "Setting device screen lock to the other device's screen lock."); getFragmentManager().beginTransaction().add(saveAndFinishWorker, null) .commit(); getFragmentManager().executePendingTransactions(); @@ -619,14 +630,14 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { saveAndFinishWorker.start( mLockPatternUtils, /* requestGatekeeperPassword= */ true, - mDeviceCredentialGuess, + mRemoteLockscreenValidationFragment.getLockscreenCredential(), /* currentCredential= */ null, mEffectiveUserId); - return; + } else { + mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(), + /* timeoutMs= */ 0, mEffectiveUserId); } - mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(), - /* timeoutMs= */ 0, mEffectiveUserId); - break; + return; case RemoteLockscreenValidationResult.RESULT_GUESS_INVALID: mCredentialCheckResultTracker.setResult(/* matched= */ false, new Intent(), /* timeoutMs= */ 0, mEffectiveUserId); @@ -636,12 +647,15 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { (int) result.getTimeoutMillis(), mEffectiveUserId); break; case RemoteLockscreenValidationResult.RESULT_NO_REMAINING_ATTEMPTS: - getActivity().finish(); - break; case RemoteLockscreenValidationResult.RESULT_SESSION_EXPIRED: - getActivity().finish(); + onRemoteLockscreenValidationFailure(String.format( + "Cannot continue remote lockscreen validation. ResultCode=%d", + result.getResultCode())); + break; } - mGlifLayout.setProgressBarShown(false); + updateRemoteLockscreenValidationViews(); + updatePasswordEntry(); + mRemoteLockscreenValidationFragment.clearLockscreenCredential(); } @Override @@ -701,21 +715,18 @@ public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity { } /** - * Callback for when the device credential guess used for remote validation was set as the - * current device's device credential. + * Callback for when the current device's lockscreen was set to the guess used for + * remote lockscreen validation. */ @Override public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) { - if (mDeviceCredentialGuess != null) { - mDeviceCredentialGuess.zeroize(); - } + Log.i(TAG, "Device lockscreen has been set to remote device's lockscreen."); + mRemoteLockscreenValidationFragment.clearLockscreenCredential(); Intent result = new Intent(); if (mRemoteValidation && containsGatekeeperPasswordHandle(resultData)) { result.putExtra(EXTRA_KEY_GK_PW_HANDLE, getGatekeeperPasswordHandle(resultData)); } - - mGlifLayout.setProgressBarShown(false); mCredentialCheckResultTracker.setResult(/* matched= */ true, result, /* timeoutMs= */ 0, mEffectiveUserId); } diff --git a/src/com/android/settings/password/ConfirmLockPattern.java b/src/com/android/settings/password/ConfirmLockPattern.java index 7c217399cd5..7db25fd830f 100644 --- a/src/com/android/settings/password/ConfirmLockPattern.java +++ b/src/com/android/settings/password/ConfirmLockPattern.java @@ -59,8 +59,6 @@ import com.android.settingslib.animation.AppearAnimationCreator; import com.android.settingslib.animation.AppearAnimationUtils; import com.android.settingslib.animation.DisappearAnimationUtils; -import com.google.android.setupdesign.GlifLayout; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -97,7 +95,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { public static class ConfirmLockPatternFragment extends ConfirmDeviceCredentialBaseFragment implements AppearAnimationCreator, CredentialCheckResultTracker.Listener, - SaveChosenLockWorkerBase.Listener { + SaveChosenLockWorkerBase.Listener, RemoteLockscreenValidationFragment.Listener { private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result"; @@ -107,7 +105,6 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { private boolean mDisappearing = false; private CountDownTimer mCountdownTimer; - private GlifLayout mGlifLayout; private View mSudContent; // caller-supplied text for various prompts @@ -239,6 +236,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { if (mCancelButton != null && TextUtils.isEmpty(mAlternateButtonText)) { mCancelButton.setText(R.string.lockpassword_forgot_pattern); } + updateRemoteLockscreenValidationViews(); } if (mForgotButton != null) { @@ -259,6 +257,9 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { mCountdownTimer.cancel(); } mCredentialCheckResultTracker.setListener(null); + if (mRemoteLockscreenValidationFragment != null) { + mRemoteLockscreenValidationFragment.setListener(null, /* handler= */ null); + } } @Override @@ -281,6 +282,12 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { updateStage(Stage.NeedToUnlock); } mCredentialCheckResultTracker.setListener(this); + if (mRemoteLockscreenValidationFragment != null) { + mRemoteLockscreenValidationFragment.setListener(this, mHandler); + if (mRemoteLockscreenValidationFragment.isRemoteValidationInProgress()) { + mLockPatternView.setEnabled(false); + } + } } @Override @@ -502,7 +509,7 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { if (mRemoteValidation) { validateGuess(credential); - mGlifLayout.setProgressBarShown(true); + updateRemoteLockscreenValidationViews(); return; } @@ -617,11 +624,12 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { } @Override - protected void onRemoteDeviceCredentialValidationResult( + public void onRemoteLockscreenValidationResult( RemoteLockscreenValidationResult result) { switch (result.getResultCode()) { case RemoteLockscreenValidationResult.RESULT_GUESS_VALID: - if (mCheckBox.isChecked()) { + if (mCheckBox.isChecked() && mRemoteLockscreenValidationFragment + .getLockscreenCredential() != null) { Log.i(TAG, "Setting device screen lock to the other device's screen lock."); ChooseLockPattern.SaveAndFinishWorker saveAndFinishWorker = new ChooseLockPattern.SaveAndFinishWorker(); @@ -632,14 +640,14 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { saveAndFinishWorker.start( mLockPatternUtils, /* requestGatekeeperPassword= */ true, - mDeviceCredentialGuess, + mRemoteLockscreenValidationFragment.getLockscreenCredential(), /* currentCredential= */ null, mEffectiveUserId); - return; + } else { + mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(), + /* timeoutMs= */ 0, mEffectiveUserId); } - mCredentialCheckResultTracker.setResult(/* matched= */ true, new Intent(), - /* timeoutMs= */ 0, mEffectiveUserId); - break; + return; case RemoteLockscreenValidationResult.RESULT_GUESS_INVALID: mCredentialCheckResultTracker.setResult(/* matched= */ false, new Intent(), /* timeoutMs= */ 0, mEffectiveUserId); @@ -649,12 +657,14 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { (int) result.getTimeoutMillis(), mEffectiveUserId); break; case RemoteLockscreenValidationResult.RESULT_NO_REMAINING_ATTEMPTS: - getActivity().finish(); - break; case RemoteLockscreenValidationResult.RESULT_SESSION_EXPIRED: - getActivity().finish(); + onRemoteLockscreenValidationFailure(String.format( + "Cannot continue remote lockscreen validation. ResultCode=%d", + result.getResultCode())); + break; } - mGlifLayout.setProgressBarShown(false); + updateRemoteLockscreenValidationViews(); + mRemoteLockscreenValidationFragment.clearLockscreenCredential(); } @Override @@ -728,21 +738,18 @@ public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity { } /** - * Callback for when the device credential guess used for remote validation was set as the - * current device's device credential. + * Callback for when the current device's lockscreen to the guess used for + * remote lockscreen validation. */ @Override public void onChosenLockSaveFinished(boolean wasSecureBefore, Intent resultData) { - if (mDeviceCredentialGuess != null) { - mDeviceCredentialGuess.zeroize(); - } + Log.i(TAG, "Device lockscreen has been set to remote device's lockscreen."); + mRemoteLockscreenValidationFragment.clearLockscreenCredential(); Intent result = new Intent(); if (mRemoteValidation && containsGatekeeperPasswordHandle(resultData)) { result.putExtra(EXTRA_KEY_GK_PW_HANDLE, getGatekeeperPasswordHandle(resultData)); } - - mGlifLayout.setProgressBarShown(false); mCredentialCheckResultTracker.setResult(/* matched= */ true, result, /* timeoutMs= */ 0, mEffectiveUserId); } diff --git a/src/com/android/settings/password/RemoteLockscreenValidationFragment.java b/src/com/android/settings/password/RemoteLockscreenValidationFragment.java new file mode 100644 index 00000000000..5819376c433 --- /dev/null +++ b/src/com/android/settings/password/RemoteLockscreenValidationFragment.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2023 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.settings.password; + +import android.app.RemoteLockscreenValidationResult; +import android.os.Bundle; +import android.os.Handler; +import android.service.remotelockscreenvalidation.IRemoteLockscreenValidationCallback; +import android.service.remotelockscreenvalidation.RemoteLockscreenValidationClient; +import android.util.Log; + +import androidx.fragment.app.Fragment; + +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.LockscreenCredential; +import com.android.security.SecureBox; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; + +/** + * A fragment used to hold state for remote lockscreen validation. + * If the original listener is ever re-created, the new listener must be set again using + * {@link #setListener} so that the validation result does not get handled by the old listener. + */ +public class RemoteLockscreenValidationFragment extends Fragment { + + private static final String TAG = RemoteLockscreenValidationFragment.class.getSimpleName(); + + private Listener mListener; + private Handler mHandler; + private boolean mIsInProgress; + private RemoteLockscreenValidationResult mResult; + private String mErrorMessage; + private LockscreenCredential mLockscreenCredential; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setRetainInstance(true); + } + + @Override + public void onDestroy() { + clearLockscreenCredential(); + if (mResult != null && mErrorMessage != null) { + Log.w(TAG, "Unprocessed remote lockscreen validation result"); + } + super.onDestroy(); + } + + /** + * @return {@code true} if remote lockscreen guess validation has started or + * the validation result has not yet been handled. + */ + public boolean isRemoteValidationInProgress() { + return mIsInProgress; + } + + /** + * Sets the listener and handler that will handle the result of remote lockscreen validation. + * Unprocessed results or failures will be handled after the listener is set. + */ + public void setListener(Listener listener, Handler handler) { + if (mListener == listener) { + return; + } + + mListener = listener; + mHandler = handler; + + if (mResult != null) { + handleResult(); + } else if (mErrorMessage != null) { + handleFailure(); + } + } + + /** + * @return {@link LockscreenCredential} if it was cached in {@link #validateLockscreenGuess}. + */ + public LockscreenCredential getLockscreenCredential() { + return mLockscreenCredential; + } + + /** + * Clears the {@link LockscreenCredential} if it was cached in {@link #validateLockscreenGuess}. + */ + public void clearLockscreenCredential() { + if (mLockscreenCredential != null) { + mLockscreenCredential.zeroize(); + mLockscreenCredential = null; + } + } + + /** + * Validates the lockscreen guess on the remote device. + * @param remoteLockscreenValidationClient the client that should be used to send the guess to + * for validation + * @param guess the {@link LockscreenCredential} guess that the user entered + * @param encryptionKey the key that should be used to encrypt the guess before validation + * @param shouldCacheGuess whether to cache to guess so it can be used to set the current + * device's lockscreen after validation succeeds. + */ + public void validateLockscreenGuess( + RemoteLockscreenValidationClient remoteLockscreenValidationClient, + LockscreenCredential guess, byte[] encryptionKey, boolean shouldCacheGuess) { + if (shouldCacheGuess) { + mLockscreenCredential = guess; + } + + remoteLockscreenValidationClient.validateLockscreenGuess( + encryptDeviceCredentialGuess(guess.getCredential(), encryptionKey), + new IRemoteLockscreenValidationCallback.Stub() { + @Override + public void onSuccess(RemoteLockscreenValidationResult result) { + mResult = result; + handleResult(); + } + + @Override + public void onFailure(String message) { + mErrorMessage = message; + handleFailure(); + } + }); + mIsInProgress = true; + } + + private byte[] encryptDeviceCredentialGuess(byte[] guess, byte[] encryptionKey) { + try { + PublicKey publicKey = SecureBox.decodePublicKey(encryptionKey); + return SecureBox.encrypt( + publicKey, + /* sharedSecret= */ null, + LockPatternUtils.ENCRYPTED_REMOTE_CREDENTIALS_HEADER, + guess); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + Log.w(TAG, "Error encrypting device credential guess. Returning empty byte[].", e); + return new byte[0]; + } + } + + private void handleResult() { + if (mHandler != null) { + mHandler.post(()-> { + if (mListener == null || mResult == null) { + return; + } + mIsInProgress = false; + mListener.onRemoteLockscreenValidationResult(mResult); + mResult = null; + }); + } + } + + private void handleFailure() { + if (mHandler != null) { + mHandler.post(()-> { + if (mListener == null || mErrorMessage == null) { + return; + } + mIsInProgress = false; + mListener.onRemoteLockscreenValidationFailure( + String.format("Remote lockscreen validation failed: %s", mErrorMessage)); + mErrorMessage = null; + }); + } + } + + interface Listener { + void onRemoteLockscreenValidationResult(RemoteLockscreenValidationResult result); + void onRemoteLockscreenValidationFailure(String message); + } +} diff --git a/tests/robotests/src/com/android/settings/password/ConfirmLockPasswordTest.java b/tests/robotests/src/com/android/settings/password/ConfirmLockPasswordTest.java index 77a013d5924..d26c33b6e68 100644 --- a/tests/robotests/src/com/android/settings/password/ConfirmLockPasswordTest.java +++ b/tests/robotests/src/com/android/settings/password/ConfirmLockPasswordTest.java @@ -197,7 +197,7 @@ public class ConfirmLockPasswordTest { verify(mCredentialCheckResultTracker).setResult( eq(true), any(), eq(0), eq(fragment.mEffectiveUserId)); assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isTrue(); - assertThat(fragment.mDeviceCredentialGuess).isNotNull(); + assertThat(fragment.mRemoteLockscreenValidationFragment.getLockscreenCredential()).isNull(); } @Test @@ -223,7 +223,7 @@ public class ConfirmLockPasswordTest { verify(mCredentialCheckResultTracker).setResult( eq(true), any(), eq(0), eq(fragment.mEffectiveUserId)); assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse(); - assertThat(fragment.mDeviceCredentialGuess).isNull(); + assertThat(fragment.mRemoteLockscreenValidationFragment.getLockscreenCredential()).isNull(); } @Test diff --git a/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java b/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java index 4374e388b79..2ed79885430 100644 --- a/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java +++ b/tests/robotests/src/com/android/settings/password/ConfirmLockPatternTest.java @@ -177,7 +177,7 @@ public class ConfirmLockPatternTest { verify(mCredentialCheckResultTracker).setResult( eq(true), any(), eq(0), eq(fragment.mEffectiveUserId)); assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isTrue(); - assertThat(fragment.mDeviceCredentialGuess).isNotNull(); + assertThat(fragment.mRemoteLockscreenValidationFragment.getLockscreenCredential()).isNull(); } @Test @@ -203,7 +203,7 @@ public class ConfirmLockPatternTest { verify(mCredentialCheckResultTracker).setResult( eq(true), any(), eq(0), eq(fragment.mEffectiveUserId)); assertThat(mLockPatternUtils.isSecure(fragment.mEffectiveUserId)).isFalse(); - assertThat(fragment.mDeviceCredentialGuess).isNull(); + assertThat(fragment.mRemoteLockscreenValidationFragment.getLockscreenCredential()).isNull(); } @Test