diff --git a/core/api/current.txt b/core/api/current.txt index f71393114444c..70dcd542d715b 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -83,6 +83,7 @@ package android { field public static final String CLEAR_APP_CACHE = "android.permission.CLEAR_APP_CACHE"; field public static final String CONFIGURE_WIFI_DISPLAY = "android.permission.CONFIGURE_WIFI_DISPLAY"; field public static final String CONTROL_LOCATION_UPDATES = "android.permission.CONTROL_LOCATION_UPDATES"; + field public static final String CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS = "android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS"; field public static final String CREDENTIAL_MANAGER_SET_ORIGIN = "android.permission.CREDENTIAL_MANAGER_SET_ORIGIN"; field public static final String DELETE_CACHE_FILES = "android.permission.DELETE_CACHE_FILES"; field public static final String DELETE_PACKAGES = "android.permission.DELETE_PACKAGES"; @@ -13674,8 +13675,9 @@ package android.credentials { } public final class CredentialOption implements android.os.Parcelable { - ctor public CredentialOption(@NonNull String, @NonNull android.os.Bundle, @NonNull android.os.Bundle, boolean); + ctor @Deprecated public CredentialOption(@NonNull String, @NonNull android.os.Bundle, @NonNull android.os.Bundle, boolean); method public int describeContents(); + method @NonNull public java.util.Set getAllowedProviders(); method @NonNull public android.os.Bundle getCandidateQueryData(); method @NonNull public android.os.Bundle getCredentialRetrievalData(); method @NonNull public String getType(); @@ -13685,6 +13687,14 @@ package android.credentials { field public static final String FLATTENED_REQUEST = "android.credentials.GetCredentialOption.FLATTENED_REQUEST_STRING"; } + public static final class CredentialOption.Builder { + ctor public CredentialOption.Builder(@NonNull String, @NonNull android.os.Bundle, @NonNull android.os.Bundle); + method @NonNull public android.credentials.CredentialOption.Builder addAllowedProvider(@NonNull android.content.ComponentName); + method @NonNull public android.credentials.CredentialOption build(); + method @NonNull public android.credentials.CredentialOption.Builder setAllowedProviders(@NonNull java.util.Set); + method @NonNull public android.credentials.CredentialOption.Builder setIsSystemProviderRequired(boolean); + } + public class GetCredentialException extends java.lang.Exception { ctor public GetCredentialException(@NonNull String, @Nullable String); ctor public GetCredentialException(@NonNull String, @Nullable String, @Nullable Throwable); diff --git a/core/java/android/credentials/CredentialOption.java b/core/java/android/credentials/CredentialOption.java index 9a3b46dad2e7a..da6656a0222ff 100644 --- a/core/java/android/credentials/CredentialOption.java +++ b/core/java/android/credentials/CredentialOption.java @@ -16,16 +16,25 @@ package android.credentials; +import static android.Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS; + import static java.util.Objects.requireNonNull; import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.content.ComponentName; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.util.ArraySet; + +import androidx.annotation.RequiresPermission; import com.android.internal.util.AnnotationValidations; import com.android.internal.util.Preconditions; +import java.util.Set; + /** * Information about a specific type of credential to be requested during a {@link * CredentialManager#getCredential(GetCredentialRequest, Activity, CancellationSignal, Executor, @@ -65,6 +74,14 @@ public final class CredentialOption implements Parcelable { */ private final boolean mIsSystemProviderRequired; + /** + * A list of {@link ComponentName}s corresponding to the providers that this option must be + * queried against. + */ + @NonNull + private final ArraySet mAllowedProviders; + + /** * Returns the requested credential type. */ @@ -105,12 +122,22 @@ public final class CredentialOption implements Parcelable { return mIsSystemProviderRequired; } + /** + * Returns the set of {@link ComponentName} corresponding to providers that must receive + * this option. + */ + @NonNull + public Set getAllowedProviders() { + return mAllowedProviders; + } + @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString8(mType); dest.writeBundle(mCredentialRetrievalData); dest.writeBundle(mCandidateQueryData); dest.writeBoolean(mIsSystemProviderRequired); + dest.writeArraySet(mAllowedProviders); } @Override @@ -125,6 +152,7 @@ public final class CredentialOption implements Parcelable { + ", requestData=" + mCredentialRetrievalData + ", candidateQueryData=" + mCandidateQueryData + ", isSystemProviderRequired=" + mIsSystemProviderRequired + + ", allowedProviders=" + mAllowedProviders + "}"; } @@ -139,17 +167,50 @@ public final class CredentialOption implements Parcelable { * provider * @throws IllegalArgumentException If type is empty. */ - public CredentialOption( + private CredentialOption( @NonNull String type, @NonNull Bundle credentialRetrievalData, @NonNull Bundle candidateQueryData, - boolean isSystemProviderRequired) { + boolean isSystemProviderRequired, + @NonNull ArraySet allowedProviders) { mType = Preconditions.checkStringNotEmpty(type, "type must not be empty"); mCredentialRetrievalData = requireNonNull(credentialRetrievalData, "requestData must not be null"); mCandidateQueryData = requireNonNull(candidateQueryData, "candidateQueryData must not be null"); mIsSystemProviderRequired = isSystemProviderRequired; + mAllowedProviders = requireNonNull(allowedProviders, "providerFilterSer must" + + "not be empty"); + } + + /** + * Constructs a {@link CredentialOption}. + * + * @param type the requested credential type + * @param credentialRetrievalData the request data + * @param candidateQueryData the partial request data that will be sent to the provider + * during the initial credential candidate query stage + * @param isSystemProviderRequired whether the request must only be fulfilled by a system + * provider + * @throws IllegalArgumentException If type is empty, or null. + * @throws NullPointerException If {@code credentialRetrievalData}, or + * {@code candidateQueryData} is null. + * + * @deprecated replaced by Builder + */ + @Deprecated + public CredentialOption( + @NonNull String type, + @NonNull Bundle credentialRetrievalData, + @NonNull Bundle candidateQueryData, + boolean isSystemProviderRequired) { + this( + type, + credentialRetrievalData, + candidateQueryData, + isSystemProviderRequired, + new ArraySet<>() + ); } private CredentialOption(@NonNull Parcel in) { @@ -165,6 +226,8 @@ public final class CredentialOption implements Parcelable { mCandidateQueryData = candidateQueryData; AnnotationValidations.validate(NonNull.class, null, mCandidateQueryData); mIsSystemProviderRequired = isSystemProviderRequired; + mAllowedProviders = (ArraySet) in.readArraySet(null); + AnnotationValidations.validate(NonNull.class, null, mAllowedProviders); } @NonNull @@ -179,4 +242,108 @@ public final class CredentialOption implements Parcelable { return new CredentialOption(in); } }; + + /** A builder for {@link CredentialOption}. */ + public static final class Builder { + + @NonNull + private String mType; + + @NonNull + private Bundle mCredentialRetrievalData; + + @NonNull + private Bundle mCandidateQueryData; + + private boolean mIsSystemProviderRequired = false; + + @NonNull + private ArraySet mAllowedProviders = new ArraySet<>(); + + /** + * @param type the type of the credential option + * @param credentialRetrievalData the full request data + * @param candidateQueryData the partial request data that will be sent to the provider + * during the initial credential candidate query stage. + * @throws IllegalArgumentException If {@code type} is null, or empty + * @throws NullPointerException If {@code credentialRetrievalData}, or + * {@code candidateQueryData} is null + */ + public Builder(@NonNull String type, @NonNull Bundle credentialRetrievalData, + @NonNull Bundle candidateQueryData) { + mType = Preconditions.checkStringNotEmpty(type, "type must not be " + + "null, or empty"); + mCredentialRetrievalData = requireNonNull(credentialRetrievalData, + "credentialRetrievalData must not be null"); + mCandidateQueryData = requireNonNull(candidateQueryData, + "candidateQueryData must not be null"); + } + + /** + * Sets a true/false value corresponding to whether this option must be serviced by + * system credentials providers only. + */ + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public Builder setIsSystemProviderRequired(boolean isSystemProviderRequired) { + mIsSystemProviderRequired = isSystemProviderRequired; + return this; + } + + /** + * Adds a provider {@link ComponentName} to be queried while gathering credentials from + * credential providers on the device. + * + * If no candidate providers are specified, all user configured and system credential + * providers will be queried in the candidate query phase. + * + * If an invalid component name is provided, or a service corresponding to the + * component name does not exist on the device, that component name is ignored. + * If all component names are invalid, or not present on the device, no providers + * are queried and no credentials are retrieved. + * + * @throws NullPointerException If {@code allowedProvider} is null + */ + @RequiresPermission(CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS) + @NonNull + public Builder addAllowedProvider(@NonNull ComponentName allowedProvider) { + mAllowedProviders.add(requireNonNull(allowedProvider, + "allowedProvider must not be null")); + return this; + } + + /** + * Sets a set of provider {@link ComponentName} to be queried while gathering credentials + * from credential providers on the device. + * + * If no candidate providers are specified, all user configured and system credential + * providers will be queried in the candidate query phase. + * + * If an invalid component name is provided, or a service corresponding to the + * component name does not exist on the device, that component name is ignored. + * If all component names are invalid, or not present on the device, no providers + * are queried and no credentials are retrieved. + * + * @throws NullPointerException If {@code allowedProviders} is null, or any of its + * elements are null. + */ + @RequiresPermission(CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS) + @NonNull + public Builder setAllowedProviders(@NonNull Set allowedProviders) { + Preconditions.checkCollectionElementsNotNull( + allowedProviders, + /*valueName=*/ "allowedProviders"); + mAllowedProviders = new ArraySet<>(allowedProviders); + return this; + } + + /** + * Builds a {@link CredentialOption}. + */ + @NonNull + public CredentialOption build() { + return new CredentialOption(mType, mCredentialRetrievalData, mCandidateQueryData, + mIsSystemProviderRequired, mAllowedProviders); + } + } } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 092f6e50aea11..0f1e558c98e16 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4482,6 +4482,12 @@ + + + { services.add( - new CredentialManagerServiceImpl(this, mLock, resolvedUserId, info)); + new CredentialManagerServiceImpl(this, mLock, resolvedUserId, + info)); }); return services; } @@ -418,6 +420,7 @@ public final class CredentialManagerService // Check privileged permissions mContext.enforceCallingPermission(CREDENTIAL_MANAGER_SET_ORIGIN, null); } + enforcePermissionForAllowedProviders(request); final int userId = UserHandle.getCallingUserId(); final int callingUid = Binder.getCallingUid(); @@ -447,6 +450,9 @@ public final class CredentialManagerService // TODO(b/273308895): implement ICancellationSignal cancelTransport = CancellationSignal.createTransport(); + + enforcePermissionForAllowedProviders(request); + return cancelTransport; } @@ -823,6 +829,17 @@ public final class CredentialManagerService } } + private void enforcePermissionForAllowedProviders(GetCredentialRequest request) { + boolean containsAllowedProviders = request.getCredentialOptions() + .stream() + .anyMatch(option -> option.getAllowedProviders() != null + && !option.getAllowedProviders().isEmpty()); + if (containsAllowedProviders) { + mContext.enforceCallingPermission(CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS, + null); + } + } + private void enforceCallingPackage(String callingPackage, int callingUid) { int packageUid; PackageManager pm = mContext.createContextAsUser( diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 9bc5998802bd7..95b0ff024bfc6 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -94,7 +94,8 @@ public final class ProviderGetSession extends ProviderSession beginGetOptionToCredentialOptionMap = new HashMap<>(); @@ -142,17 +143,19 @@ public final class ProviderGetSession extends ProviderSession providerCapabilities, - android.credentials.GetCredentialRequest clientRequest + android.credentials.GetCredentialRequest clientRequest, + ComponentName componentName ) { List filteredOptions = new ArrayList<>(); for (CredentialOption option : clientRequest.getCredentialOptions()) { - if (providerCapabilities.contains(option.getType())) { + if (providerCapabilities.contains(option.getType()) + && isProviderAllowed(option, componentName)) { Log.i(TAG, "In createProviderRequest - capability found : " + option.getType()); filteredOptions.add(option); } else { Log.i(TAG, "In createProviderRequest - capability not " - + "found : " + option.getType()); + + "found, or provider not allowed : " + option.getType()); } } if (!filteredOptions.isEmpty()) { @@ -165,6 +168,16 @@ public final class ProviderGetSession extends ProviderSession callbacks,