tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

WebAuthnCredentialManager.java (17938B)


      1 /* -*- Mode: Java; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil -*- */
      2 /* vim: set ts=2 et sw=2 tw=100: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 package org.mozilla.gecko;
      8 
      9 import android.annotation.SuppressLint;
     10 import android.content.Context;
     11 import android.content.pm.PackageManager;
     12 import android.credentials.CreateCredentialException;
     13 import android.credentials.CreateCredentialRequest;
     14 import android.credentials.CreateCredentialResponse;
     15 import android.credentials.CredentialManager;
     16 import android.credentials.CredentialOption;
     17 import android.credentials.GetCredentialException;
     18 import android.credentials.GetCredentialRequest;
     19 import android.credentials.GetCredentialResponse;
     20 import android.credentials.PrepareGetCredentialResponse;
     21 import android.os.Build;
     22 import android.os.Bundle;
     23 import android.os.OutcomeReceiver;
     24 import android.util.Base64;
     25 import android.util.Log;
     26 import org.json.JSONException;
     27 import org.json.JSONObject;
     28 import org.mozilla.gecko.util.GeckoBundle;
     29 import org.mozilla.gecko.util.WebAuthnUtils;
     30 import org.mozilla.geckoview.GeckoResult;
     31 
     32 // Credential Manager implementation that is introduced from Android 14.
     33 public class WebAuthnCredentialManager {
     34  private static final String LOGTAG = "WebAuthnCredMan";
     35  private static final boolean DEBUG = false;
     36 
     37  // This defines are from androidx.credentials.
     38  // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:credentials/credentials/src/main/java/androidx/credentials/
     39  private static final String CM_PREFIX = "androidx.credentials.";
     40  private static final String TYPE_PUBLIC_KEY_CREDENTIAL = CM_PREFIX + "TYPE_PUBLIC_KEY_CREDENTIAL";
     41  private static final String BUNDLE_KEY_CLIENT_DATA_HASH =
     42      CM_PREFIX + "BUNDLE_KEY_CLIENT_DATA_HASH";
     43  private static final String BUNDLE_KEY_REGISTRATION_RESPONSE_JSON =
     44      CM_PREFIX + "BUNDLE_KEY_REGISTRATION_RESPONSE_JSON";
     45  private static final String BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON =
     46      CM_PREFIX + "BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON";
     47  private static final String BUNDLE_KEY_REQUEST_JSON = CM_PREFIX + "BUNDLE_KEY_REQUEST_JSON";
     48  private static final String BUNDLE_KEY_REQUEST_DISPLAY_INFO =
     49      CM_PREFIX + "BUNDLE_KEY_REQUEST_DISPLAY_INFO";
     50  private static final String BUNDLE_KEY_SUBTYPE = CM_PREFIX + "BUNDLE_KEY_SUBTYPE";
     51  private static final String BUNDLE_KEY_USER_DISPLAY_NAME =
     52      CM_PREFIX + "BUNDLE_KEY_USER_DISPLAY_NAME";
     53  private static final String BUNDLE_KEY_USER_ID = CM_PREFIX + "BUNDLE_KEY_USER_ID";
     54  private static final String BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST =
     55      CM_PREFIX + "BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST";
     56  private static final String BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION =
     57      CM_PREFIX + "BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION";
     58  private static final String BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED =
     59      CM_PREFIX + "BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED";
     60  private static final String BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS =
     61      CM_PREFIX + "BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS";
     62 
     63  private static Bundle getRequestBundle(final String requestJSON, final byte[] clientDataHash) {
     64    final Bundle requestBundle = new Bundle();
     65    requestBundle.putString(BUNDLE_KEY_REQUEST_JSON, requestJSON);
     66    requestBundle.putByteArray(BUNDLE_KEY_CLIENT_DATA_HASH, clientDataHash);
     67 
     68    return requestBundle;
     69  }
     70 
     71  private static Bundle getRequestBundleForMakeCredential(
     72      final GeckoBundle credentialBundle,
     73      final byte[] userId,
     74      final byte[] challenge,
     75      final int[] algs,
     76      final WebAuthnUtils.WebAuthnPublicCredential[] excludeList,
     77      final GeckoBundle authenticatorSelection,
     78      final GeckoBundle extensions,
     79      final byte[] clientDataHash) {
     80    try {
     81      final JSONObject requestJSON =
     82          WebAuthnUtils.getJSONObjectForMakeCredential(
     83              credentialBundle,
     84              userId,
     85              challenge,
     86              algs,
     87              excludeList,
     88              authenticatorSelection,
     89              extensions);
     90      final Bundle bundle = getRequestBundle(requestJSON.toString(), clientDataHash);
     91      if (bundle == null) {
     92        return null;
     93      }
     94 
     95      final Bundle displayInfoBundle = new Bundle();
     96      displayInfoBundle.putCharSequence(
     97          BUNDLE_KEY_USER_ID,
     98          Base64.encodeToString(userId, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP));
     99      final GeckoBundle userBundle = credentialBundle.getBundle("user");
    100      displayInfoBundle.putString(
    101          BUNDLE_KEY_USER_DISPLAY_NAME, userBundle.getString("displayName", ""));
    102      bundle.putBundle(BUNDLE_KEY_REQUEST_DISPLAY_INFO, displayInfoBundle);
    103 
    104      bundle.putString(
    105          BUNDLE_KEY_SUBTYPE, BUNDLE_VALUE_SUBTYPE_CREATE_PUBLIC_KEY_CREDENTIAL_REQUEST);
    106      return bundle;
    107    } catch (final JSONException e) {
    108      Log.e(LOGTAG, "Couldn't generate JSON object for request", e);
    109    }
    110    return null;
    111  }
    112 
    113  private static Bundle getRequestBundleForGetAssertion(
    114      final byte[] challenge,
    115      final WebAuthnUtils.WebAuthnPublicCredential[] allowList,
    116      final GeckoBundle assertionBundle,
    117      final GeckoBundle extensionsBundle,
    118      final byte[] clientDataHash) {
    119    try {
    120      final JSONObject requestJSON =
    121          WebAuthnUtils.getJSONObjectForGetAssertion(
    122              challenge, allowList, assertionBundle, extensionsBundle);
    123      final Bundle bundle = getRequestBundle(requestJSON.toString(), clientDataHash);
    124      if (bundle == null) {
    125        return null;
    126      }
    127      bundle.putString(BUNDLE_KEY_SUBTYPE, BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION);
    128      return bundle;
    129    } catch (final JSONException e) {
    130      Log.e(LOGTAG, "Couldn't generate JSON object for request", e);
    131    }
    132    return null;
    133  }
    134 
    135  @SuppressLint("MissingPermission")
    136  public static GeckoResult<WebAuthnUtils.MakeCredentialResponse> makeCredential(
    137      final GeckoBundle credentialBundle,
    138      final byte[] userId,
    139      final byte[] challenge,
    140      final int[] algs,
    141      final WebAuthnUtils.WebAuthnPublicCredential[] excludeList,
    142      final GeckoBundle authenticatorSelection,
    143      final GeckoBundle extensions,
    144      final byte[] clientDataHash) {
    145 
    146    // We use Credential Manager first. If it doesn't work, we use GMS FIDO2.
    147    // Credential manager may support non-discoverable keys,
    148    // Else, following the specifications, `residentKey=discouraged` allows discoverable keys too
    149    // but prefer non-discoverable keys
    150    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
    151      return GeckoResult.fromException(new WebAuthnUtils.Exception("NOT_SUPPORTED_ERR"));
    152    }
    153    final Context context = GeckoAppShell.getApplicationContext();
    154    // Some vendors disabled Credential Manager on Android 14+ devices.
    155    // https://issuetracker.google.com/issues/349310440
    156    if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CREDENTIALS)) {
    157      Log.w(LOGTAG, "Credential Manager is disabled on this device");
    158      return GeckoResult.fromException(new WebAuthnUtils.Exception("NOT_SUPPORTED_ERR"));
    159    }
    160 
    161    final Bundle requestBundle =
    162        getRequestBundleForMakeCredential(
    163            credentialBundle,
    164            userId,
    165            challenge,
    166            algs,
    167            excludeList,
    168            authenticatorSelection,
    169            extensions,
    170            clientDataHash);
    171    if (requestBundle == null) {
    172      return GeckoResult.fromException(new WebAuthnUtils.Exception("UNKNOWN_ERR"));
    173    }
    174 
    175    requestBundle.putBoolean(BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS, false);
    176    requestBundle.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, false);
    177 
    178    final CreateCredentialRequest request =
    179        new CreateCredentialRequest.Builder(
    180                TYPE_PUBLIC_KEY_CREDENTIAL, requestBundle, requestBundle)
    181            .setAlwaysSendAppInfoToProvider(true)
    182            .setOrigin(credentialBundle.getString("origin"))
    183            .build();
    184 
    185    final CredentialManager manager =
    186        (CredentialManager) context.getSystemService(Context.CREDENTIAL_SERVICE);
    187    if (manager == null) {
    188      return GeckoResult.fromException(new WebAuthnUtils.Exception("UNKNOWN_ERR"));
    189    }
    190    final GeckoResult<WebAuthnUtils.MakeCredentialResponse> result = new GeckoResult<>();
    191    try {
    192      manager.createCredential(
    193          context,
    194          request,
    195          null,
    196          context.getMainExecutor(),
    197          new OutcomeReceiver<CreateCredentialResponse, CreateCredentialException>() {
    198            @Override
    199            public void onResult(final CreateCredentialResponse createCredentialResponse) {
    200              final Bundle data = createCredentialResponse.getData();
    201              final String responseJson = data.getString(BUNDLE_KEY_REGISTRATION_RESPONSE_JSON);
    202              if (responseJson == null) {
    203                result.completeExceptionally(new WebAuthnUtils.Exception("DATA_ERR"));
    204                return;
    205              }
    206              if (DEBUG) {
    207                Log.d(LOGTAG, "Response JSON: " + responseJson);
    208              }
    209              try {
    210                result.complete(WebAuthnUtils.getMakeCredentialResponse(responseJson));
    211              } catch (final IllegalArgumentException e) {
    212                Log.e(LOGTAG, "Invalid response", e);
    213                result.completeExceptionally(new WebAuthnUtils.Exception("DATA_ERR"));
    214              } catch (final JSONException e) {
    215                Log.e(LOGTAG, "Couldn't parse response JSON", e);
    216                result.completeExceptionally(new WebAuthnUtils.Exception("DATA_ERR"));
    217              }
    218            }
    219 
    220            @Override
    221            public void onError(final CreateCredentialException exception) {
    222              final String errorType = exception.getType();
    223              if (DEBUG) {
    224                Log.d(LOGTAG, "Couldn't create credential. errorType=" + errorType);
    225              }
    226              if (errorType.equals(CreateCredentialException.TYPE_USER_CANCELED)) {
    227                result.completeExceptionally(new WebAuthnUtils.Exception("ABORT_ERR"));
    228                return;
    229              }
    230              if (errorType.equals(CreateCredentialException.TYPE_NO_CREATE_OPTIONS)) {
    231                result.completeExceptionally(new WebAuthnUtils.Exception("NOT_SUPPORTED_ERR"));
    232                return;
    233              }
    234              result.completeExceptionally(new WebAuthnUtils.Exception("UNKNOWN_ERR"));
    235            }
    236          });
    237    } catch (final SecurityException e) {
    238      // We might be no permission for Credential Manager
    239      return GeckoResult.fromException(new WebAuthnUtils.Exception("NOT_SUPPORTED_ERR"));
    240    } catch (final java.lang.Exception e) {
    241      Log.w(LOGTAG, "Couldn't make credential", e);
    242      return GeckoResult.fromException(new WebAuthnUtils.Exception("UNKNOWN_ERR"));
    243    }
    244    return result;
    245  }
    246 
    247  @SuppressLint("MissingPermission")
    248  public static GeckoResult<PrepareGetCredentialResponse.PendingGetCredentialHandle>
    249      prepareGetAssertion(
    250          final byte[] challenge,
    251          final WebAuthnUtils.WebAuthnPublicCredential[] allowList,
    252          final GeckoBundle assertionBundle,
    253          final GeckoBundle extensions,
    254          final byte[] clientDataHash) {
    255    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
    256      // No credential manager. Relay to GMS FIDO2
    257      return GeckoResult.fromValue(null);
    258    }
    259    final Bundle requestBundle =
    260        getRequestBundleForGetAssertion(
    261            challenge, allowList, assertionBundle, extensions, clientDataHash);
    262    if (requestBundle == null) {
    263      return GeckoResult.fromValue(null);
    264    }
    265 
    266    requestBundle.putBoolean(BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED, allowList.length > 0);
    267 
    268    final CredentialOption credentialOption =
    269        new CredentialOption.Builder(TYPE_PUBLIC_KEY_CREDENTIAL, requestBundle, requestBundle)
    270            .build();
    271    final Bundle bundle = new Bundle();
    272    final GetCredentialRequest request =
    273        new GetCredentialRequest.Builder(requestBundle)
    274            .addCredentialOption(credentialOption)
    275            .setAlwaysSendAppInfoToProvider(true)
    276            .setOrigin(assertionBundle.getString("origin"))
    277            .build();
    278 
    279    final Context context = GeckoAppShell.getApplicationContext();
    280    final CredentialManager manager =
    281        (CredentialManager) context.getSystemService(Context.CREDENTIAL_SERVICE);
    282    if (manager == null) {
    283      // No credential manager. Relay to GMS FIDO2
    284      return GeckoResult.fromValue(null);
    285    }
    286 
    287    final GeckoResult<PrepareGetCredentialResponse.PendingGetCredentialHandle> result =
    288        new GeckoResult<>();
    289    try {
    290      manager.prepareGetCredential(
    291          request,
    292          null,
    293          context.getMainExecutor(),
    294          new OutcomeReceiver<PrepareGetCredentialResponse, GetCredentialException>() {
    295            @Override
    296            public void onResult(final PrepareGetCredentialResponse prepareGetCredentialResponse) {
    297              final boolean hasPublicKeyCredentials =
    298                  prepareGetCredentialResponse.hasCredentialResults(TYPE_PUBLIC_KEY_CREDENTIAL);
    299              final boolean hasAuthenticationResults =
    300                  prepareGetCredentialResponse.hasAuthenticationResults();
    301 
    302              if (DEBUG) {
    303                Log.d(
    304                    LOGTAG,
    305                    "prepareGetCredential: hasPublicKeyCredentials="
    306                        + hasPublicKeyCredentials
    307                        + ", hasAuthenticationResults="
    308                        + hasAuthenticationResults);
    309              }
    310              if (!hasPublicKeyCredentials && !hasAuthenticationResults) {
    311                // No passkey and result. Relay to GMS FIDO2.
    312                result.complete(null);
    313                return;
    314              }
    315 
    316              result.complete(prepareGetCredentialResponse.getPendingGetCredentialHandle());
    317            }
    318 
    319            @Override
    320            public void onError(final GetCredentialException exception) {
    321              if (DEBUG) {
    322                final String errorType = exception.getType();
    323                Log.d(LOGTAG, "Couldn't get credential. errorType=" + errorType);
    324              }
    325              result.completeExceptionally(new WebAuthnUtils.Exception("UNKNOWN_ERR"));
    326            }
    327          });
    328    } catch (final SecurityException e) {
    329      // We might be no permission for Credential Manager. Use FIDO2 API
    330      return GeckoResult.fromValue(null);
    331    } catch (final java.lang.Exception e) {
    332      Log.e(LOGTAG, "prepareGetCredential throws an error", e);
    333      return GeckoResult.fromValue(null);
    334    }
    335    return result;
    336  }
    337 
    338  public static GeckoResult<WebAuthnUtils.GetAssertionResponse> getAssertion(
    339      final PrepareGetCredentialResponse.PendingGetCredentialHandle pendingHandle) {
    340    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
    341      return GeckoResult.fromException(new WebAuthnUtils.Exception("NOT_SUPPORTED_ERR"));
    342    }
    343    final GeckoResult<WebAuthnUtils.GetAssertionResponse> result = new GeckoResult<>();
    344    final Context context = GeckoAppShell.getApplicationContext();
    345    final CredentialManager manager =
    346        (CredentialManager) context.getSystemService(Context.CREDENTIAL_SERVICE);
    347    if (manager == null) {
    348      return GeckoResult.fromException(new WebAuthnUtils.Exception("UNKNOWN_ERR"));
    349    }
    350    try {
    351      manager.getCredential(
    352          context,
    353          pendingHandle,
    354          null,
    355          context.getMainExecutor(),
    356          new OutcomeReceiver<GetCredentialResponse, GetCredentialException>() {
    357            @Override
    358            public void onResult(final GetCredentialResponse getCredentialResponse) {
    359              final Bundle data = getCredentialResponse.getCredential().getData();
    360              final String responseJson = data.getString(BUNDLE_KEY_AUTHENTICATION_RESPONSE_JSON);
    361              if (responseJson == null) {
    362                result.completeExceptionally(new WebAuthnUtils.Exception("DATA_ERR"));
    363                return;
    364              }
    365              if (DEBUG) {
    366                Log.d(LOGTAG, "Response JSON: " + responseJson);
    367              }
    368              try {
    369                result.complete(WebAuthnUtils.getGetAssertionResponse(responseJson));
    370              } catch (final IllegalArgumentException e) {
    371                Log.e(LOGTAG, "Invalid response", e);
    372                result.completeExceptionally(new WebAuthnUtils.Exception("DATA_ERR"));
    373              } catch (final JSONException e) {
    374                Log.e(LOGTAG, "Couldn't parse response JSON", e);
    375                result.completeExceptionally(new WebAuthnUtils.Exception("DATA_ERR"));
    376              }
    377            }
    378 
    379            @Override
    380            public void onError(final GetCredentialException exception) {
    381              final String errorType = exception.getType();
    382              if (DEBUG) {
    383                Log.d(LOGTAG, "Couldn't get credential. errorType=" + errorType);
    384              }
    385              if (errorType.equals(GetCredentialException.TYPE_USER_CANCELED)) {
    386                result.completeExceptionally(new WebAuthnUtils.Exception("ABORT_ERR"));
    387                return;
    388              }
    389              result.completeExceptionally(new WebAuthnUtils.Exception("UNKNOWN_ERR"));
    390            }
    391          });
    392    } catch (final SecurityException e) {
    393      // We might be no permission for Credential Manager.
    394      return GeckoResult.fromException(new WebAuthnUtils.Exception("NOT_SUPPORTED_ERR"));
    395    } catch (final java.lang.Exception e) {
    396      Log.w(LOGTAG, "Couldn't get assertion", e);
    397      return GeckoResult.fromException(new WebAuthnUtils.Exception("UNKNOWN_ERR"));
    398    }
    399    return result;
    400  }
    401 }