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 entitity - 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. Device attestation,
45  * e.g. {@link java.security.KeyStore#getCertificateChain getCertificateChain()}, is used to
46  * generate a certificate chain that includes the public key (<code>Kpub</code> in the following)
47  * 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>A common way of implementing the "<code>promptText</code> is what is expected" check in the
82  * last bullet, is to have the <i>Relying Party</i> generate <code>promptText</code> and store it
83  * along the nonce in the <code>extraData</code> blob.
84  */
85 public class ConfirmationPrompt {
86     private static final String TAG = "ConfirmationPrompt";
87 
88     private CharSequence mPromptText;
89     private byte[] mExtraData;
90     private ConfirmationCallback mCallback;
91     private Executor mExecutor;
92     private Context mContext;
93 
94     private final KeyStore mKeyStore = KeyStore.getInstance();
95 
doCallback(int responseCode, byte[] dataThatWasConfirmed, ConfirmationCallback callback)96     private void doCallback(int responseCode, byte[] dataThatWasConfirmed,
97             ConfirmationCallback callback) {
98         switch (responseCode) {
99             case KeyStore.CONFIRMATIONUI_OK:
100                 callback.onConfirmed(dataThatWasConfirmed);
101                 break;
102 
103             case KeyStore.CONFIRMATIONUI_CANCELED:
104                 callback.onDismissed();
105                 break;
106 
107             case KeyStore.CONFIRMATIONUI_ABORTED:
108                 callback.onCanceled();
109                 break;
110 
111             case KeyStore.CONFIRMATIONUI_SYSTEM_ERROR:
112                 callback.onError(new Exception("System error returned by ConfirmationUI."));
113                 break;
114 
115             default:
116                 callback.onError(new Exception("Unexpected responseCode=" + responseCode
117                                 + " from onConfirmtionPromptCompleted() callback."));
118                 break;
119         }
120     }
121 
122     private final android.os.IBinder mCallbackBinder =
123             new android.security.IConfirmationPromptCallback.Stub() {
124                 @Override
125                 public void onConfirmationPromptCompleted(
126                         int responseCode, final byte[] dataThatWasConfirmed)
127                         throws android.os.RemoteException {
128                     if (mCallback != null) {
129                         ConfirmationCallback callback = mCallback;
130                         Executor executor = mExecutor;
131                         mCallback = null;
132                         mExecutor = null;
133                         if (executor == null) {
134                             doCallback(responseCode, dataThatWasConfirmed, callback);
135                         } else {
136                             executor.execute(new Runnable() {
137                                     @Override
138                                     public void run() {
139                                         doCallback(responseCode, dataThatWasConfirmed, callback);
140                                     }
141                                 });
142                         }
143                     }
144                 }
145             };
146 
147     /**
148      * A builder that collects arguments, to be shown on the system-provided confirmation prompt.
149      */
150     public static final class Builder {
151 
152         private Context mContext;
153         private CharSequence mPromptText;
154         private byte[] mExtraData;
155 
156         /**
157          * Creates a builder for the confirmation prompt.
158          *
159          * @param context the application context
160          */
Builder(Context context)161         public Builder(Context context) {
162             mContext = context;
163         }
164 
165         /**
166          * Sets the prompt text for the prompt.
167          *
168          * @param promptText the text to present in the prompt.
169          * @return the builder.
170          */
setPromptText(CharSequence promptText)171         public Builder setPromptText(CharSequence promptText) {
172             mPromptText = promptText;
173             return this;
174         }
175 
176         /**
177          * Sets the extra data for the prompt.
178          *
179          * @param extraData data to include in the response data.
180          * @return the builder.
181          */
setExtraData(byte[] extraData)182         public Builder setExtraData(byte[] extraData) {
183             mExtraData = extraData;
184             return this;
185         }
186 
187         /**
188          * Creates a {@link ConfirmationPrompt} with the arguments supplied to this builder.
189          *
190          * @return a {@link ConfirmationPrompt}
191          * @throws IllegalArgumentException if any of the required fields are not set.
192          */
build()193         public ConfirmationPrompt build() {
194             if (TextUtils.isEmpty(mPromptText)) {
195                 throw new IllegalArgumentException("prompt text must be set and non-empty");
196             }
197             if (mExtraData == null) {
198                 throw new IllegalArgumentException("extraData must be set");
199             }
200             return new ConfirmationPrompt(mContext, mPromptText, mExtraData);
201         }
202     }
203 
ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData)204     private ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData) {
205         mContext = context;
206         mPromptText = promptText;
207         mExtraData = extraData;
208     }
209 
210     private static final int UI_OPTION_ACCESSIBILITY_INVERTED_FLAG = 1 << 0;
211     private static final int UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG = 1 << 1;
212 
getUiOptionsAsFlags()213     private int getUiOptionsAsFlags() {
214         int uiOptionsAsFlags = 0;
215         ContentResolver contentResolver = mContext.getContentResolver();
216         int inversionEnabled = Settings.Secure.getInt(contentResolver,
217                 Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0);
218         if (inversionEnabled == 1) {
219             uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_INVERTED_FLAG;
220         }
221         float fontScale = Settings.System.getFloat(contentResolver,
222                 Settings.System.FONT_SCALE, (float) 1.0);
223         if (fontScale > 1.0) {
224             uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG;
225         }
226         return uiOptionsAsFlags;
227     }
228 
isAccessibilityServiceRunning(Context context)229     private static boolean isAccessibilityServiceRunning(Context context) {
230         boolean serviceRunning = false;
231         try {
232             ContentResolver contentResolver = context.getContentResolver();
233             int a11yEnabled = Settings.Secure.getInt(contentResolver,
234                     Settings.Secure.ACCESSIBILITY_ENABLED);
235             if (a11yEnabled == 1) {
236                 serviceRunning = true;
237             }
238         } catch (SettingNotFoundException e) {
239             Log.w(TAG, "Unexpected SettingNotFoundException");
240             e.printStackTrace();
241         }
242         return serviceRunning;
243     }
244 
245     /**
246      * Requests a confirmation prompt to be presented to the user.
247      *
248      * When the prompt is no longer being presented, one of the methods in
249      * {@link ConfirmationCallback} is called on the supplied callback object.
250      *
251      * Confirmation prompts may not be available when accessibility services are running so this
252      * may fail with a {@link ConfirmationNotAvailableException} exception even if
253      * {@link #isSupported} returns {@code true}.
254      *
255      * @param executor the executor identifying the thread that will receive the callback.
256      * @param callback the callback to use when the prompt is done showing.
257      * @throws IllegalArgumentException if the prompt text is too long or malfomed.
258      * @throws ConfirmationAlreadyPresentingException if another prompt is being presented.
259      * @throws ConfirmationNotAvailableException if confirmation prompts are not supported.
260      */
presentPrompt(@onNull Executor executor, @NonNull ConfirmationCallback callback)261     public void presentPrompt(@NonNull Executor executor, @NonNull ConfirmationCallback callback)
262             throws ConfirmationAlreadyPresentingException,
263             ConfirmationNotAvailableException {
264         if (mCallback != null) {
265             throw new ConfirmationAlreadyPresentingException();
266         }
267         if (isAccessibilityServiceRunning(mContext)) {
268             throw new ConfirmationNotAvailableException();
269         }
270         mCallback = callback;
271         mExecutor = executor;
272 
273         int uiOptionsAsFlags = getUiOptionsAsFlags();
274         String locale = Locale.getDefault().toLanguageTag();
275         int responseCode = mKeyStore.presentConfirmationPrompt(
276                 mCallbackBinder, mPromptText.toString(), mExtraData, locale, uiOptionsAsFlags);
277         switch (responseCode) {
278             case KeyStore.CONFIRMATIONUI_OK:
279                 return;
280 
281             case KeyStore.CONFIRMATIONUI_OPERATION_PENDING:
282                 throw new ConfirmationAlreadyPresentingException();
283 
284             case KeyStore.CONFIRMATIONUI_UNIMPLEMENTED:
285                 throw new ConfirmationNotAvailableException();
286 
287             case KeyStore.CONFIRMATIONUI_UIERROR:
288                 throw new IllegalArgumentException();
289 
290             default:
291                 // Unexpected error code.
292                 Log.w(TAG,
293                         "Unexpected responseCode=" + responseCode
294                         + " from presentConfirmationPrompt() call.");
295                 throw new IllegalArgumentException();
296         }
297     }
298 
299     /**
300      * Cancels a prompt currently being displayed.
301      *
302      * On success, the
303      * {@link ConfirmationCallback#onCanceled onCanceled()} method on
304      * the supplied callback object will be called asynchronously.
305      *
306      * @throws IllegalStateException if no prompt is currently being presented.
307      */
cancelPrompt()308     public void cancelPrompt() {
309         int responseCode = mKeyStore.cancelConfirmationPrompt(mCallbackBinder);
310         if (responseCode == KeyStore.CONFIRMATIONUI_OK) {
311             return;
312         } else if (responseCode == KeyStore.CONFIRMATIONUI_OPERATION_PENDING) {
313             throw new IllegalStateException();
314         } else {
315             // Unexpected error code.
316             Log.w(TAG,
317                     "Unexpected responseCode=" + responseCode
318                     + " from cancelConfirmationPrompt() call.");
319             throw new IllegalStateException();
320         }
321     }
322 
323     /**
324      * Checks if the device supports confirmation prompts.
325      *
326      * @param context the application context.
327      * @return true if confirmation prompts are supported by the device.
328      */
isSupported(Context context)329     public static boolean isSupported(Context context) {
330         if (isAccessibilityServiceRunning(context)) {
331             return false;
332         }
333         return KeyStore.getInstance().isConfirmationPromptSupported();
334     }
335 }
336