1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.password;
18 
19 import android.app.RemoteLockscreenValidationResult;
20 import android.os.Bundle;
21 import android.os.Handler;
22 import android.service.remotelockscreenvalidation.IRemoteLockscreenValidationCallback;
23 import android.service.remotelockscreenvalidation.RemoteLockscreenValidationClient;
24 import android.util.Log;
25 
26 import androidx.fragment.app.Fragment;
27 
28 import com.android.internal.widget.LockPatternUtils;
29 import com.android.internal.widget.LockscreenCredential;
30 import com.android.security.SecureBox;
31 
32 import java.security.InvalidKeyException;
33 import java.security.NoSuchAlgorithmException;
34 import java.security.PublicKey;
35 
36 /**
37  * A fragment used to hold state for remote lockscreen validation.
38  * If the original listener is ever re-created, the new listener must be set again using
39  * {@link #setListener} so that the validation result does not get handled by the old listener.
40  */
41 public class RemoteLockscreenValidationFragment extends Fragment {
42 
43     private static final String TAG = RemoteLockscreenValidationFragment.class.getSimpleName();
44 
45     private Listener mListener;
46     private Handler mHandler;
47     private boolean mIsInProgress;
48     private RemoteLockscreenValidationResult mResult;
49     private String mErrorMessage;
50     private LockscreenCredential mLockscreenCredential;
51 
52     @Override
onCreate(Bundle savedInstanceState)53     public void onCreate(Bundle savedInstanceState) {
54         super.onCreate(savedInstanceState);
55         setRetainInstance(true);
56     }
57 
58     @Override
onDestroy()59     public void onDestroy() {
60         clearLockscreenCredential();
61         if (mResult != null && mErrorMessage != null) {
62             Log.w(TAG, "Unprocessed remote lockscreen validation result");
63         }
64         super.onDestroy();
65     }
66 
67     /**
68      * @return {@code true} if remote lockscreen guess validation has started or
69      * the validation result has not yet been handled.
70      */
isRemoteValidationInProgress()71     public boolean isRemoteValidationInProgress() {
72         return mIsInProgress;
73     }
74 
75     /**
76      * Sets the listener and handler that will handle the result of remote lockscreen validation.
77      * Unprocessed results or failures will be handled after the listener is set.
78      */
setListener(Listener listener, Handler handler)79     public void setListener(Listener listener, Handler handler) {
80         if (mListener == listener) {
81             return;
82         }
83 
84         mListener = listener;
85         mHandler = handler;
86 
87         if (mResult != null) {
88             handleResult();
89         } else if (mErrorMessage != null) {
90             handleFailure();
91         }
92     }
93 
94     /**
95      * @return {@link LockscreenCredential} if it was cached in {@link #validateLockscreenGuess}.
96      */
getLockscreenCredential()97     public LockscreenCredential getLockscreenCredential() {
98         return mLockscreenCredential;
99     }
100 
101     /**
102      * Clears the {@link LockscreenCredential} if it was cached in {@link #validateLockscreenGuess}.
103      */
clearLockscreenCredential()104     public void clearLockscreenCredential() {
105         if (mLockscreenCredential != null) {
106             mLockscreenCredential.zeroize();
107             mLockscreenCredential = null;
108         }
109     }
110 
111     /**
112      * Validates the lockscreen guess on the remote device.
113      * @param remoteLockscreenValidationClient the client that should be used to send the guess to
114      *                                         for validation
115      * @param guess the {@link LockscreenCredential} guess that the user entered
116      * @param encryptionKey the key that should be used to encrypt the guess before validation
117      * @param shouldCacheGuess whether to cache to guess so it can be used to set the current
118      *                         device's lockscreen after validation succeeds.
119      */
validateLockscreenGuess( RemoteLockscreenValidationClient remoteLockscreenValidationClient, LockscreenCredential guess, byte[] encryptionKey, boolean shouldCacheGuess)120     public void validateLockscreenGuess(
121             RemoteLockscreenValidationClient remoteLockscreenValidationClient,
122             LockscreenCredential guess, byte[] encryptionKey, boolean shouldCacheGuess) {
123         if (shouldCacheGuess) {
124             mLockscreenCredential = guess;
125         }
126 
127         remoteLockscreenValidationClient.validateLockscreenGuess(
128                 encryptDeviceCredentialGuess(guess.getCredential(), encryptionKey),
129                 new IRemoteLockscreenValidationCallback.Stub() {
130                     @Override
131                     public void onSuccess(RemoteLockscreenValidationResult result) {
132                         mResult = result;
133                         handleResult();
134                     }
135 
136                     @Override
137                     public void onFailure(String message) {
138                         mErrorMessage = message;
139                         handleFailure();
140                     }
141                 });
142         mIsInProgress = true;
143     }
144 
encryptDeviceCredentialGuess(byte[] guess, byte[] encryptionKey)145     private byte[] encryptDeviceCredentialGuess(byte[] guess, byte[] encryptionKey) {
146         try {
147             PublicKey publicKey = SecureBox.decodePublicKey(encryptionKey);
148             return SecureBox.encrypt(
149                     publicKey,
150                     /* sharedSecret= */ null,
151                     LockPatternUtils.ENCRYPTED_REMOTE_CREDENTIALS_HEADER,
152                     guess);
153         } catch (NoSuchAlgorithmException | InvalidKeyException e) {
154             Log.w(TAG, "Error encrypting device credential guess. Returning empty byte[].", e);
155             return new byte[0];
156         }
157     }
158 
handleResult()159     private void handleResult() {
160         if (mHandler != null) {
161             mHandler.post(()-> {
162                 if (mListener == null || mResult == null) {
163                     return;
164                 }
165                 mIsInProgress = false;
166                 mListener.onRemoteLockscreenValidationResult(mResult);
167                 mResult = null;
168             });
169         }
170     }
171 
handleFailure()172     private void handleFailure() {
173         if (mHandler != null) {
174             mHandler.post(()-> {
175                 if (mListener == null || mErrorMessage == null) {
176                     return;
177                 }
178                 mIsInProgress = false;
179                 mListener.onRemoteLockscreenValidationFailure(
180                         String.format("Remote lockscreen validation failed: %s", mErrorMessage));
181                 mErrorMessage = null;
182             });
183         }
184     }
185 
186     interface Listener {
onRemoteLockscreenValidationResult(RemoteLockscreenValidationResult result)187         void onRemoteLockscreenValidationResult(RemoteLockscreenValidationResult result);
onRemoteLockscreenValidationFailure(String message)188         void onRemoteLockscreenValidationFailure(String message);
189     }
190 }
191