Home | History | Annotate | Download | only in security
      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 
     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          */
    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          */
    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          */
    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          */
    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 
    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 
    213     private int getUiOptionsAsFlags() {
    214         int uiOptionsAsFlags = 0;
    215         try {
    216             ContentResolver contentResolver = mContext.getContentResolver();
    217             int inversionEnabled = Settings.Secure.getInt(contentResolver,
    218                     Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED);
    219             if (inversionEnabled == 1) {
    220                 uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_INVERTED_FLAG;
    221             }
    222             float fontScale = Settings.System.getFloat(contentResolver,
    223                     Settings.System.FONT_SCALE);
    224             if (fontScale > 1.0) {
    225                 uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG;
    226             }
    227         } catch (SettingNotFoundException e) {
    228             Log.w(TAG, "Unexpected SettingNotFoundException");
    229         }
    230         return uiOptionsAsFlags;
    231     }
    232 
    233     private static boolean isAccessibilityServiceRunning(Context context) {
    234         boolean serviceRunning = false;
    235         try {
    236             ContentResolver contentResolver = context.getContentResolver();
    237             int a11yEnabled = Settings.Secure.getInt(contentResolver,
    238                     Settings.Secure.ACCESSIBILITY_ENABLED);
    239             if (a11yEnabled == 1) {
    240                 serviceRunning = true;
    241             }
    242         } catch (SettingNotFoundException e) {
    243             Log.w(TAG, "Unexpected SettingNotFoundException");
    244             e.printStackTrace();
    245         }
    246         return serviceRunning;
    247     }
    248 
    249     /**
    250      * Requests a confirmation prompt to be presented to the user.
    251      *
    252      * When the prompt is no longer being presented, one of the methods in
    253      * {@link ConfirmationCallback} is called on the supplied callback object.
    254      *
    255      * Confirmation prompts may not be available when accessibility services are running so this
    256      * may fail with a {@link ConfirmationNotAvailableException} exception even if
    257      * {@link #isSupported} returns {@code true}.
    258      *
    259      * @param executor the executor identifying the thread that will receive the callback.
    260      * @param callback the callback to use when the prompt is done showing.
    261      * @throws IllegalArgumentException if the prompt text is too long or malfomed.
    262      * @throws ConfirmationAlreadyPresentingException if another prompt is being presented.
    263      * @throws ConfirmationNotAvailableException if confirmation prompts are not supported.
    264      */
    265     public void presentPrompt(@NonNull Executor executor, @NonNull ConfirmationCallback callback)
    266             throws ConfirmationAlreadyPresentingException,
    267             ConfirmationNotAvailableException {
    268         if (mCallback != null) {
    269             throw new ConfirmationAlreadyPresentingException();
    270         }
    271         if (isAccessibilityServiceRunning(mContext)) {
    272             throw new ConfirmationNotAvailableException();
    273         }
    274         mCallback = callback;
    275         mExecutor = executor;
    276 
    277         int uiOptionsAsFlags = getUiOptionsAsFlags();
    278         String locale = Locale.getDefault().toLanguageTag();
    279         int responseCode = mKeyStore.presentConfirmationPrompt(
    280                 mCallbackBinder, mPromptText.toString(), mExtraData, locale, uiOptionsAsFlags);
    281         switch (responseCode) {
    282             case KeyStore.CONFIRMATIONUI_OK:
    283                 return;
    284 
    285             case KeyStore.CONFIRMATIONUI_OPERATION_PENDING:
    286                 throw new ConfirmationAlreadyPresentingException();
    287 
    288             case KeyStore.CONFIRMATIONUI_UNIMPLEMENTED:
    289                 throw new ConfirmationNotAvailableException();
    290 
    291             case KeyStore.CONFIRMATIONUI_UIERROR:
    292                 throw new IllegalArgumentException();
    293 
    294             default:
    295                 // Unexpected error code.
    296                 Log.w(TAG,
    297                         "Unexpected responseCode=" + responseCode
    298                         + " from presentConfirmationPrompt() call.");
    299                 throw new IllegalArgumentException();
    300         }
    301     }
    302 
    303     /**
    304      * Cancels a prompt currently being displayed.
    305      *
    306      * On success, the
    307      * {@link ConfirmationCallback#onCanceled onCanceled()} method on
    308      * the supplied callback object will be called asynchronously.
    309      *
    310      * @throws IllegalStateException if no prompt is currently being presented.
    311      */
    312     public void cancelPrompt() {
    313         int responseCode = mKeyStore.cancelConfirmationPrompt(mCallbackBinder);
    314         if (responseCode == KeyStore.CONFIRMATIONUI_OK) {
    315             return;
    316         } else if (responseCode == KeyStore.CONFIRMATIONUI_OPERATION_PENDING) {
    317             throw new IllegalStateException();
    318         } else {
    319             // Unexpected error code.
    320             Log.w(TAG,
    321                     "Unexpected responseCode=" + responseCode
    322                     + " from cancelConfirmationPrompt() call.");
    323             throw new IllegalStateException();
    324         }
    325     }
    326 
    327     /**
    328      * Checks if the device supports confirmation prompts.
    329      *
    330      * @param context the application context.
    331      * @return true if confirmation prompts are supported by the device.
    332      */
    333     public static boolean isSupported(Context context) {
    334         if (isAccessibilityServiceRunning(context)) {
    335             return false;
    336         }
    337         return KeyStore.getInstance().isConfirmationPromptSupported();
    338     }
    339 }
    340