1 /*
2  * Copyright 2018 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 android.security;
18 
19 import android.annotation.NonNull;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.provider.Settings;
23 import android.provider.Settings.SettingNotFoundException;
24 import android.text.TextUtils;
25 import android.util.Log;
26 
27 import java.util.Locale;
28 import java.util.concurrent.Executor;
29 
30 /**
31  * Class used for displaying confirmation prompts.
32  *
33  * <p>Confirmation prompts are prompts shown to the user to confirm a given text and are
34  * implemented in a way that a positive response indicates with high confidence that the user has
35  * seen the given text, even if the Android framework (including the kernel) was
36  * compromised. Implementing confirmation prompts with these guarantees requires dedicated
37  * hardware-support and may not always be available.
38  *
39  * <p>Confirmation prompts are typically used with an external entity - the <i>Relying Party</i> -
40  * in the following way. The setup steps are as follows:
41  * <ul>
42  * <li> Before first use, the application generates a key-pair with the
43  * {@link android.security.keystore.KeyGenParameterSpec.Builder#setUserConfirmationRequired
44  * CONFIRMATION tag} set. AndroidKeyStore key attestation, e.g.,
45  * {@link android.security.keystore.KeyGenParameterSpec.Builder#setAttestationChallenge(byte[])}
46  * is used to generate a certificate chain that includes the public key (<code>Kpub</code> in the
47  * following) of the newly generated key.
48  * <li> The application sends <code>Kpub</code> and the certificate chain resulting from device
49  * attestation to the <i>Relying Party</i>.
50  * <li> The <i>Relying Party</i> validates the certificate chain which involves checking the root
51  * certificate is what is expected (e.g. a certificate from Google), each certificate signs the
52  * next one in the chain, ending with <code>Kpub</code>, and that the attestation certificate
53  * asserts that <code>Kpub</code> has the
54  * {@link android.security.keystore.KeyGenParameterSpec.Builder#setUserConfirmationRequired
55  * CONFIRMATION tag} set.
56  * Additionally the relying party stores <code>Kpub</code> and associates it with the device
57  * it was received from.
58  * </ul>
59  *
60  * <p>The <i>Relying Party</i> is typically an external device (for example connected via
61  * Bluetooth) or application server.
62  *
63  * <p>Before executing a transaction which requires a high assurance of user content, the
64  * application does the following:
65  * <ul>
66  * <li> The application gets a cryptographic nonce from the <i>Relying Party</i> and passes this as
67  * the <code>extraData</code> (via the Builder helper class) to the
68  * {@link #presentPrompt presentPrompt()} method. The <i>Relying Party</i> stores the nonce locally
69  * since it'll use it in a later step.
70  * <li> If the user approves the prompt a <i>Confirmation Response</i> is returned in the
71  * {@link ConfirmationCallback#onConfirmed onConfirmed(byte[])} callback as the
72  * <code>dataThatWasConfirmed</code> parameter. This blob contains the text that was shown to the
73  * user, the <code>extraData</code> parameter, and possibly other data.
74  * <li> The application signs the <i>Confirmation Response</i> with the previously created key and
75  * sends the blob and the signature to the <i>Relying Party</i>.
76  * <li> The <i>Relying Party</i> checks that the signature was made with <code>Kpub</code> and then
77  * extracts <code>promptText</code> matches what is expected and <code>extraData</code> matches the
78  * previously created nonce. If all checks passes, the transaction is executed.
79  * </ul>
80  *
81  * <p>Note: It is vital to check the <code>promptText</code> because this is the only part that
82  * the user has approved. To avoid writing parsers for all of the possible locales, it is
83  * recommended that the <i>Relying Party</i> uses the same string generator as used on the device
84  * and performs a simple string comparison.
85  */
86 public class ConfirmationPrompt {
87     private static final String TAG = "ConfirmationPrompt";
88 
89     private CharSequence mPromptText;
90     private byte[] mExtraData;
91     private ConfirmationCallback mCallback;
92     private Executor mExecutor;
93     private Context mContext;
94 
95     private AndroidProtectedConfirmation mProtectedConfirmation;
96 
getService()97     private AndroidProtectedConfirmation getService() {
98         if (mProtectedConfirmation == null) {
99             mProtectedConfirmation = new AndroidProtectedConfirmation();
100         }
101         return mProtectedConfirmation;
102     }
103 
doCallback(int responseCode, byte[] dataThatWasConfirmed, ConfirmationCallback callback)104     private void doCallback(int responseCode, byte[] dataThatWasConfirmed,
105             ConfirmationCallback callback) {
106         switch (responseCode) {
107             case AndroidProtectedConfirmation.ERROR_OK:
108                 callback.onConfirmed(dataThatWasConfirmed);
109                 break;
110 
111             case AndroidProtectedConfirmation.ERROR_CANCELED:
112                 callback.onDismissed();
113                 break;
114 
115             case AndroidProtectedConfirmation.ERROR_ABORTED:
116                 callback.onCanceled();
117                 break;
118 
119             case AndroidProtectedConfirmation.ERROR_SYSTEM_ERROR:
120                 callback.onError(new Exception("System error returned by ConfirmationUI."));
121                 break;
122 
123             default:
124                 callback.onError(new Exception("Unexpected responseCode=" + responseCode
125                         + " from onConfirmtionPromptCompleted() callback."));
126                 break;
127         }
128     }
129 
130     private final android.security.apc.IConfirmationCallback mConfirmationCallback =
131             new android.security.apc.IConfirmationCallback.Stub() {
132                 @Override
133                 public void onCompleted(int result, byte[] dataThatWasConfirmed)
134                         throws android.os.RemoteException {
135                     if (mCallback != null) {
136                         ConfirmationCallback callback = mCallback;
137                         Executor executor = mExecutor;
138                         mCallback = null;
139                         mExecutor = null;
140                         if (executor == null) {
141                             doCallback(result, dataThatWasConfirmed, callback);
142                         } else {
143                             executor.execute(new Runnable() {
144                                 @Override public void run() {
145                                     doCallback(result, dataThatWasConfirmed, callback);
146                                 }
147                             });
148                         }
149                     }
150                 }
151             };
152 
153     /**
154      * A builder that collects arguments, to be shown on the system-provided confirmation prompt.
155      */
156     public static final class Builder {
157 
158         private Context mContext;
159         private CharSequence mPromptText;
160         private byte[] mExtraData;
161 
162         /**
163          * Creates a builder for the confirmation prompt.
164          *
165          * @param context the application context
166          */
Builder(Context context)167         public Builder(Context context) {
168             mContext = context;
169         }
170 
171         /**
172          * Sets the prompt text for the prompt.
173          *
174          * @param promptText the text to present in the prompt.
175          * @return the builder.
176          */
setPromptText(CharSequence promptText)177         public Builder setPromptText(CharSequence promptText) {
178             mPromptText = promptText;
179             return this;
180         }
181 
182         /**
183          * Sets the extra data for the prompt.
184          *
185          * @param extraData data to include in the response data.
186          * @return the builder.
187          */
setExtraData(byte[] extraData)188         public Builder setExtraData(byte[] extraData) {
189             mExtraData = extraData;
190             return this;
191         }
192 
193         /**
194          * Creates a {@link ConfirmationPrompt} with the arguments supplied to this builder.
195          *
196          * @return a {@link ConfirmationPrompt}
197          * @throws IllegalArgumentException if any of the required fields are not set.
198          */
build()199         public ConfirmationPrompt build() {
200             if (TextUtils.isEmpty(mPromptText)) {
201                 throw new IllegalArgumentException("prompt text must be set and non-empty");
202             }
203             if (mExtraData == null) {
204                 throw new IllegalArgumentException("extraData must be set");
205             }
206             return new ConfirmationPrompt(mContext, mPromptText, mExtraData);
207         }
208     }
209 
ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData)210     private ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData) {
211         mContext = context;
212         mPromptText = promptText;
213         mExtraData = extraData;
214     }
215 
getUiOptionsAsFlags()216     private int getUiOptionsAsFlags() {
217         int uiOptionsAsFlags = 0;
218         ContentResolver contentResolver = mContext.getContentResolver();
219         int inversionEnabled = Settings.Secure.getInt(contentResolver,
220                 Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0);
221         if (inversionEnabled == 1) {
222             uiOptionsAsFlags |= AndroidProtectedConfirmation.FLAG_UI_OPTION_INVERTED;
223         }
224         float fontScale = Settings.System.getFloat(contentResolver,
225                 Settings.System.FONT_SCALE, (float) 1.0);
226         if (fontScale > 1.0) {
227             uiOptionsAsFlags |= AndroidProtectedConfirmation.FLAG_UI_OPTION_MAGNIFIED;
228         }
229         return uiOptionsAsFlags;
230     }
231 
isAccessibilityServiceRunning(Context context)232     private static boolean isAccessibilityServiceRunning(Context context) {
233         boolean serviceRunning = false;
234         try {
235             ContentResolver contentResolver = context.getContentResolver();
236             int a11yEnabled = Settings.Secure.getInt(contentResolver,
237                     Settings.Secure.ACCESSIBILITY_ENABLED);
238             if (a11yEnabled == 1) {
239                 serviceRunning = true;
240             }
241         } catch (SettingNotFoundException e) {
242             Log.w(TAG, "Unexpected SettingNotFoundException");
243             e.printStackTrace();
244         }
245         return serviceRunning;
246     }
247 
248     /**
249      * Requests a confirmation prompt to be presented to the user.
250      *
251      * When the prompt is no longer being presented, one of the methods in
252      * {@link ConfirmationCallback} is called on the supplied callback object.
253      *
254      * Confirmation prompts may not be available when accessibility services are running so this
255      * may fail with a {@link ConfirmationNotAvailableException} exception even if
256      * {@link #isSupported} returns {@code true}.
257      *
258      * @param executor the executor identifying the thread that will receive the callback.
259      * @param callback the callback to use when the prompt is done showing.
260      * @throws IllegalArgumentException if the prompt text is too long or malfomed.
261      * @throws ConfirmationAlreadyPresentingException if another prompt is being presented.
262      * @throws ConfirmationNotAvailableException if confirmation prompts are not supported.
263      */
presentPrompt(@onNull Executor executor, @NonNull ConfirmationCallback callback)264     public void presentPrompt(@NonNull Executor executor, @NonNull ConfirmationCallback callback)
265             throws ConfirmationAlreadyPresentingException,
266             ConfirmationNotAvailableException {
267         if (mCallback != null) {
268             throw new ConfirmationAlreadyPresentingException();
269         }
270         if (isAccessibilityServiceRunning(mContext)) {
271             throw new ConfirmationNotAvailableException();
272         }
273         mCallback = callback;
274         mExecutor = executor;
275 
276         String locale = Locale.getDefault().toLanguageTag();
277         int uiOptionsAsFlags = getUiOptionsAsFlags();
278         int responseCode = getService().presentConfirmationPrompt(
279                 mConfirmationCallback, mPromptText.toString(), mExtraData, locale,
280                 uiOptionsAsFlags);
281         switch (responseCode) {
282             case AndroidProtectedConfirmation.ERROR_OK:
283                 return;
284 
285             case AndroidProtectedConfirmation.ERROR_OPERATION_PENDING:
286                 throw new ConfirmationAlreadyPresentingException();
287 
288             case AndroidProtectedConfirmation.ERROR_UNIMPLEMENTED:
289                 throw new ConfirmationNotAvailableException();
290 
291             default:
292                 // Unexpected error code.
293                 Log.w(TAG,
294                         "Unexpected responseCode=" + responseCode
295                                 + " from presentConfirmationPrompt() call.");
296                 throw new IllegalArgumentException();
297         }
298     }
299 
300     /**
301      * Cancels a prompt currently being displayed.
302      *
303      * On success, the
304      * {@link ConfirmationCallback#onCanceled onCanceled()} method on
305      * the supplied callback object will be called asynchronously.
306      *
307      * @throws IllegalStateException if no prompt is currently being presented.
308      */
cancelPrompt()309     public void cancelPrompt() {
310         int responseCode =
311                 getService().cancelConfirmationPrompt(mConfirmationCallback);
312         if (responseCode == AndroidProtectedConfirmation.ERROR_OK) {
313             return;
314         } else if (responseCode == AndroidProtectedConfirmation.ERROR_OPERATION_PENDING) {
315             throw new IllegalStateException();
316         } else {
317             // Unexpected error code.
318             Log.w(TAG,
319                     "Unexpected responseCode=" + responseCode
320                             + " from cancelConfirmationPrompt() call.");
321             throw new IllegalStateException();
322         }
323     }
324 
325     /**
326      * Checks if the device supports confirmation prompts.
327      *
328      * @param context the application context.
329      * @return true if confirmation prompts are supported by the device.
330      */
isSupported(Context context)331     public static boolean isSupported(Context context) {
332         if (isAccessibilityServiceRunning(context)) {
333             return false;
334         }
335         return new AndroidProtectedConfirmation().isConfirmationPromptSupported();
336     }
337 }
338