Merge "Guard setting of remote entry with a permission" into udc-dev

This commit is contained in:
Reema Bajwa
2023-03-07 00:54:34 +00:00
committed by Android (Google) Code Review
10 changed files with 126 additions and 57 deletions

View File

@@ -40549,7 +40549,7 @@ package android.service.credentials {
method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder addCreateEntry(@NonNull android.service.credentials.CreateEntry);
method @NonNull public android.service.credentials.BeginCreateCredentialResponse build();
method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder setCreateEntries(@NonNull java.util.List<android.service.credentials.CreateEntry>);
method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder setRemoteCreateEntry(@Nullable android.service.credentials.RemoteEntry);
method @NonNull @RequiresPermission("android.permission.PROVIDE_REMOTE_CREDENTIALS") public android.service.credentials.BeginCreateCredentialResponse.Builder setRemoteCreateEntry(@Nullable android.service.credentials.RemoteEntry);
}
public class BeginGetCredentialOption implements android.os.Parcelable {
@@ -40598,7 +40598,7 @@ package android.service.credentials {
method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setActions(@NonNull java.util.List<android.service.credentials.Action>);
method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setAuthenticationActions(@NonNull java.util.List<android.service.credentials.Action>);
method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setCredentialEntries(@NonNull java.util.List<android.service.credentials.CredentialEntry>);
method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setRemoteCredentialEntry(@Nullable android.service.credentials.RemoteEntry);
method @NonNull @RequiresPermission("android.permission.PROVIDE_REMOTE_CREDENTIALS") public android.service.credentials.BeginGetCredentialResponse.Builder setRemoteCredentialEntry(@Nullable android.service.credentials.RemoteEntry);
}
public final class CallingAppInfo implements android.os.Parcelable {

View File

@@ -254,7 +254,7 @@ package android {
field public static final String PERFORM_SIM_ACTIVATION = "android.permission.PERFORM_SIM_ACTIVATION";
field public static final String POWER_SAVER = "android.permission.POWER_SAVER";
field public static final String PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE = "android.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE";
field public static final String PROVIDE_HYBRID_CREDENTIAL_SERVICE = "android.permission.PROVIDE_HYBRID_CREDENTIAL_SERVICE";
field public static final String PROVIDE_REMOTE_CREDENTIALS = "android.permission.PROVIDE_REMOTE_CREDENTIALS";
field public static final String PROVIDE_RESOLVER_RANKER_SERVICE = "android.permission.PROVIDE_RESOLVER_RANKER_SERVICE";
field public static final String PROVIDE_TRUST_AGENT = "android.permission.PROVIDE_TRUST_AGENT";
field public static final String PROVISION_DEMO_DEVICE = "android.permission.PROVISION_DEMO_DEVICE";

View File

@@ -16,8 +16,10 @@
package android.service.credentials;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.os.Parcel;
import android.os.Parcelable;
@@ -137,7 +139,17 @@ public final class BeginCreateCredentialResponse implements Parcelable {
* result should be set to {@link android.app.Activity#RESULT_OK} and an extra with the
* {@link CredentialProviderService#EXTRA_CREATE_CREDENTIAL_RESPONSE} key should be populated
* with a {@link android.credentials.CreateCredentialResponse} object.
*
* <p> Note that as a provider service you will only be able to set a remote entry if :
* - Provider service possesses the
* {@link Manifest.permission.PROVIDE_REMOTE_CREDENTIALS} permission.
* - Provider service is configured as the provider that can provide remote entries.
*
* If the above conditions are not met, setting back {@link BeginCreateCredentialResponse}
* on the callback from {@link CredentialProviderService#onBeginCreateCredential}
* will throw a {@link SecurityException}.
*/
@RequiresPermission(Manifest.permission.PROVIDE_REMOTE_CREDENTIALS)
public @NonNull Builder setRemoteCreateEntry(@Nullable RemoteEntry remoteCreateEntry) {
mRemoteCreateEntry = remoteCreateEntry;
return this;

View File

@@ -16,8 +16,10 @@
package android.service.credentials;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.os.Parcel;
import android.os.Parcelable;
@@ -154,7 +156,17 @@ public final class BeginGetCredentialResponse implements Parcelable {
* result should be set to {@link android.app.Activity#RESULT_OK} and an extra with the
* {@link CredentialProviderService#EXTRA_GET_CREDENTIAL_RESPONSE} key should be populated
* with a {@link android.credentials.Credential} object.
*
* <p> Note that as a provider service you will only be able to set a remote entry if :
* - Provider service possesses the
* {@link Manifest.permission.PROVIDE_REMOTE_CREDENTIALS} permission.
* - Provider service is configured as the provider that can provide remote entries.
*
* If the above conditions are not met, setting back {@link BeginGetCredentialResponse}
* on the callback from {@link CredentialProviderService#onBeginGetCredential} will
* throw a {@link SecurityException}.
*/
@RequiresPermission(Manifest.permission.PROVIDE_REMOTE_CREDENTIALS)
public @NonNull Builder setRemoteCredentialEntry(@Nullable RemoteEntry
remoteCredentialEntry) {
mRemoteCredentialEntry = remoteCredentialEntry;

View File

@@ -18,6 +18,7 @@ package android.service.credentials;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.Manifest;
import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.SdkConstant;
@@ -218,6 +219,11 @@ public abstract class CredentialProviderService extends Service {
GetCredentialException>() {
@Override
public void onResult(BeginGetCredentialResponse result) {
// If provider service does not possess the HYBRID permission, this
// check will throw an exception in the provider process.
if (result.getRemoteCredentialEntry() != null) {
enforceRemoteEntryPermission();
}
try {
callback.onSuccess(result);
} catch (RemoteException e) {
@@ -236,6 +242,15 @@ public abstract class CredentialProviderService extends Service {
));
return transport;
}
private void enforceRemoteEntryPermission() {
String permission =
Manifest.permission.PROVIDE_REMOTE_CREDENTIALS;
getApplicationContext().enforceCallingOrSelfPermission(
permission,
String.format("Provider must have %s, in order to set a "
+ "remote entry", permission)
);
}
@Override
public ICancellationSignal onBeginCreateCredential(BeginCreateCredentialRequest request,
@@ -253,6 +268,11 @@ public abstract class CredentialProviderService extends Service {
BeginCreateCredentialResponse, CreateCredentialException>() {
@Override
public void onResult(BeginCreateCredentialResponse result) {
// If provider service does not possess the HYBRID permission, this
// check will throw an exception in the provider process.
if (result.getRemoteCreateEntry() != null) {
enforceRemoteEntryPermission();
}
try {
callback.onSuccess(result);
} catch (RemoteException e) {

View File

@@ -4482,8 +4482,8 @@
<!-- Allows an application to be able to store and retrieve credentials from a remote
device.
@hide @SystemApi -->
<permission android:name="android.permission.PROVIDE_HYBRID_CREDENTIAL_SERVICE"
android:protectionLevel="signature|privileged" />
<permission android:name="android.permission.PROVIDE_REMOTE_CREDENTIALS"
android:protectionLevel="signature|privileged|role" />
<!-- ========================================= -->
<!-- Permissions for special development tools -->

View File

@@ -137,7 +137,8 @@ public final class ProviderCreateSession extends ProviderSession<
remoteCredentialService);
mCompleteRequest = completeCreateRequest;
setStatus(Status.PENDING);
mProviderResponseDataHandler = new ProviderResponseDataHandler(hybridService);
mProviderResponseDataHandler = new ProviderResponseDataHandler(
ComponentName.unflattenFromString(hybridService));
}
@Override
@@ -297,21 +298,23 @@ public final class ProviderCreateSession extends ProviderSession<
}
private class ProviderResponseDataHandler {
private final ComponentName mExpectedRemoteEntryProviderService;
@Nullable private final ComponentName mExpectedRemoteEntryProviderService;
@NonNull
private final Map<String, Pair<CreateEntry, Entry>> mUiCreateEntries = new HashMap<>();
@Nullable private Pair<String, Pair<RemoteEntry, Entry>> mUiRemoteEntry = null;
ProviderResponseDataHandler(String hybridService) {
mExpectedRemoteEntryProviderService = ComponentName.unflattenFromString(hybridService);
ProviderResponseDataHandler(@Nullable ComponentName expectedRemoteEntryProviderService) {
mExpectedRemoteEntryProviderService = expectedRemoteEntryProviderService;
}
public void addResponseContent(List<CreateEntry> createEntries,
RemoteEntry remoteEntry) {
createEntries.forEach(this::addCreateEntry);
setRemoteEntry(remoteEntry);
if (remoteEntry != null) {
setRemoteEntry(remoteEntry);
}
}
public void addCreateEntry(CreateEntry createEntry) {
String id = generateUniqueId();
@@ -321,13 +324,13 @@ public final class ProviderCreateSession extends ProviderSession<
}
public void setRemoteEntry(@Nullable RemoteEntry remoteEntry) {
if (remoteEntry == null) {
mUiRemoteEntry = null;
if (!enforceRemoteEntryRestrictions(mExpectedRemoteEntryProviderService)) {
Log.i(TAG, "Remote entry being dropped as it does not meet the restriction"
+ "checks.");
return;
}
if (!mComponentName.equals(mExpectedRemoteEntryProviderService)) {
Log.i(TAG, "Remote entry being dropped as it is not from the service "
+ "configured by the OEM.");
if (remoteEntry == null) {
mUiRemoteEntry = null;
return;
}
String id = generateUniqueId();

View File

@@ -181,7 +181,6 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
/** Called when the provider response has been updated by an external source. */
@Override // Callback from the remote provider
public void onProviderResponseSuccess(@Nullable BeginGetCredentialResponse response) {
Log.i(TAG, "in onProviderResponseSuccess");
onSetInitialRemoteResponse(response);
}
@@ -393,7 +392,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
.extractResponseContent(providerPendingIntentResponse
.getResultData());
if (response != null && !mProviderResponseDataHandler.isEmptyResponse(response)) {
addToInitialRemoteResponse(response);
addToInitialRemoteResponse(response, /*isInitialResponse=*/ false);
// Additional content received is in the form of new response content.
return true;
}
@@ -401,7 +400,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
return false;
}
private void addToInitialRemoteResponse(BeginGetCredentialResponse content) {
private void addToInitialRemoteResponse(BeginGetCredentialResponse content,
boolean isInitialResponse) {
if (content == null) {
return;
}
@@ -409,7 +409,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
content.getCredentialEntries(),
content.getActions(),
content.getAuthenticationActions(),
content.getRemoteCredentialEntry()
content.getRemoteCredentialEntry(),
isInitialResponse
);
}
@@ -424,7 +425,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
/** Updates the response being maintained in state by this provider session. */
private void onSetInitialRemoteResponse(BeginGetCredentialResponse response) {
mProviderResponse = response;
addToInitialRemoteResponse(response);
addToInitialRemoteResponse(response, /*isInitialResponse=*/true);
if (mProviderResponseDataHandler.isEmptyResponse(response)) {
updateStatusAndInvokeCallback(Status.EMPTY_RESPONSE);
return;
@@ -463,7 +464,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
}
private class ProviderResponseDataHandler {
private final ComponentName mExpectedRemoteEntryProviderService;
@Nullable private final ComponentName mExpectedRemoteEntryProviderService;
@NonNull
private final Map<String, Pair<CredentialEntry, Entry>> mUiCredentialEntries =
new HashMap<>();
@@ -475,19 +476,27 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
@Nullable private Pair<String, Pair<RemoteEntry, Entry>> mUiRemoteEntry = null;
ProviderResponseDataHandler(ComponentName expectedRemoteEntryProviderService) {
ProviderResponseDataHandler(@Nullable ComponentName expectedRemoteEntryProviderService) {
mExpectedRemoteEntryProviderService = expectedRemoteEntryProviderService;
}
public void addResponseContent(List<CredentialEntry> credentialEntries,
List<Action> actions, List<Action> authenticationActions,
RemoteEntry remoteEntry) {
RemoteEntry remoteEntry, boolean isInitialResponse) {
credentialEntries.forEach(this::addCredentialEntry);
actions.forEach(this::addAction);
authenticationActions.forEach(
authenticationAction -> addAuthenticationAction(authenticationAction,
AuthenticationEntry.STATUS_LOCKED));
setRemoteEntry(remoteEntry);
// In the query phase, it is likely most providers will return a null remote entry
// so no need to invoke the setter since it adds the overhead of checking for the
// hybrid permission, and then sets an already null value to null.
// If this is not the query phase, e.g. response after a locked entry is unlocked
// then it is valid for the provider to remove the remote entry, and so we allow
// them to set it to null.
if (remoteEntry != null || !isInitialResponse) {
setRemoteEntry(remoteEntry);
}
}
public void addCredentialEntry(CredentialEntry credentialEntry) {
String id = generateUniqueId();
@@ -524,12 +533,13 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
}
public void setRemoteEntry(@Nullable RemoteEntry remoteEntry) {
if (remoteEntry == null) {
if (!enforceRemoteEntryRestrictions(mExpectedRemoteEntryProviderService)) {
Log.i(TAG, "Remote entry being dropped as it does not meet the restriction"
+ " checks.");
return;
}
if (!mComponentName.equals(mExpectedRemoteEntryProviderService)) {
Log.i(TAG, "Remote entry being dropped as it is not from the service "
+ "configured by the OEM.");
if (remoteEntry == null) {
mUiRemoteEntry = null;
return;
}
String id = generateUniqueId();
@@ -538,6 +548,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
mUiRemoteEntry = new Pair<>(generateUniqueId(), new Pair<>(remoteEntry, entry));
}
public GetCredentialProviderData toGetCredentialProviderData() {
return new GetCredentialProviderData.Builder(
mComponentName.flattenToString()).setActionChips(prepareActionEntries())
@@ -571,7 +583,6 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
return credEntries;
}
private Entry prepareRemoteEntry() {
if (mUiRemoteEntry == null || mUiRemoteEntry.first == null
|| mUiRemoteEntry.second == null) {

View File

@@ -19,10 +19,13 @@ package com.android.server.credentials;
import static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_QUERY_FAILURE;
import static com.android.server.credentials.MetricUtilities.METRICS_PROVIDER_STATUS_QUERY_SUCCESS;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.credentials.Credential;
import android.credentials.ui.ProviderData;
import android.credentials.ui.ProviderPendingIntentResponse;
@@ -228,6 +231,39 @@ public abstract class ProviderSession<T, R>
return mProviderResponse;
}
protected boolean enforceRemoteEntryRestrictions(
@Nullable ComponentName expectedRemoteEntryProviderService) {
// Check if the service is the one set by the OEM. If not silently reject this entry
if (!mComponentName.equals(expectedRemoteEntryProviderService)) {
Log.i(TAG, "Remote entry being dropped as it is not from the service "
+ "configured by the OEM.");
return false;
}
// Check if the service has the hybrid permission .If not, silently reject this entry.
// This check is in addition to the permission check happening in the provider's process.
try {
ApplicationInfo appInfo = mContext.getPackageManager().getApplicationInfo(
mComponentName.getPackageName(),
PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY));
if (appInfo != null
&& mContext.checkPermission(
Manifest.permission.PROVIDE_REMOTE_CREDENTIALS,
/*pId=*/-1, appInfo.uid) == PackageManager.PERMISSION_GRANTED) {
return true;
}
} catch (SecurityException e) {
Log.i(TAG, "Error getting info for "
+ mComponentName.flattenToString() + ": " + e.getMessage());
return false;
} catch (PackageManager.NameNotFoundException e) {
Log.i(TAG, "Error getting info for "
+ mComponentName.flattenToString() + ": " + e.getMessage());
return false;
}
Log.i(TAG, "In enforceRemoteEntryRestrictions - remote entry checks fail");
return false;
}
/** Should be overridden to prepare, and stores state for {@link ProviderData} to be
* shown on the UI. */
@Nullable protected abstract ProviderData prepareUiData();

View File

@@ -121,8 +121,6 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr
ProviderCallbacks<BeginGetCredentialResponse> callback) {
Log.i(TAG, "In onGetCredentials in RemoteCredentialService");
AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
AtomicReference<CompletableFuture<BeginGetCredentialResponse>> futureRef =
new AtomicReference<>();
CompletableFuture<BeginGetCredentialResponse> connectThenExecute = postAsync(service -> {
CompletableFuture<BeginGetCredentialResponse> getCredentials =
@@ -134,7 +132,6 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr
new IBeginGetCredentialCallback.Stub() {
@Override
public void onSuccess(BeginGetCredentialResponse response) {
Log.i(TAG, "In onSuccess in RemoteCredentialService");
getCredentials.complete(response);
}
@@ -147,22 +144,15 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr
new GetCredentialException(errorType, errorMsg));
}
});
CompletableFuture<BeginGetCredentialResponse> future = futureRef.get();
if (future != null && future.isCancelled()) {
dispatchCancellationSignal(cancellationSignal);
} else {
cancellationSink.set(cancellationSignal);
}
cancellationSink.set(cancellationSignal);
return getCredentials;
} finally {
Binder.restoreCallingIdentity(originalCallingUidToken);
}
}).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
handleExecutionResponse(result, error, cancellationSink, callback)));
return cancellationSink.get();
}
@@ -178,8 +168,6 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr
ProviderCallbacks<BeginCreateCredentialResponse> callback) {
Log.i(TAG, "In onCreateCredential in RemoteCredentialService");
AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
AtomicReference<CompletableFuture<BeginCreateCredentialResponse>> futureRef =
new AtomicReference<>();
CompletableFuture<BeginCreateCredentialResponse> connectThenExecute =
postAsync(service -> {
@@ -205,19 +193,13 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr
new CreateCredentialException(errorType, errorMsg));
}
});
CompletableFuture<BeginCreateCredentialResponse> future = futureRef.get();
if (future != null && future.isCancelled()) {
dispatchCancellationSignal(cancellationSignal);
} else {
cancellationSink.set(cancellationSignal);
}
cancellationSink.set(cancellationSignal);
return createCredentialFuture;
} finally {
Binder.restoreCallingIdentity(originalCallingUidToken);
}
}).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
handleExecutionResponse(result, error, cancellationSink, callback)));
@@ -236,7 +218,6 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr
ProviderCallbacks<Void> callback) {
Log.i(TAG, "In onClearCredentialState in RemoteCredentialService");
AtomicReference<ICancellationSignal> cancellationSink = new AtomicReference<>();
AtomicReference<CompletableFuture<Void>> futureRef = new AtomicReference<>();
CompletableFuture<Void> connectThenExecute =
postAsync(service -> {
@@ -263,19 +244,13 @@ public class RemoteCredentialService extends ServiceConnector.Impl<ICredentialPr
errorMsg));
}
});
CompletableFuture<Void> future = futureRef.get();
if (future != null && future.isCancelled()) {
dispatchCancellationSignal(cancellationSignal);
} else {
cancellationSink.set(cancellationSignal);
}
cancellationSink.set(cancellationSignal);
return clearCredentialFuture;
} finally {
Binder.restoreCallingIdentity(originalCallingUidToken);
}
}).orTimeout(TIMEOUT_REQUEST_MILLIS, TimeUnit.MILLISECONDS);
futureRef.set(connectThenExecute);
connectThenExecute.whenComplete((result, error) -> Handler.getMain().post(() ->
handleExecutionResponse(result, error, cancellationSink, callback)));