diff --git a/core/java/android/credentials/CredentialDescription.aidl b/core/java/android/credentials/CredentialDescription.aidl new file mode 100644 index 0000000000000..1b5739e7ff163 --- /dev/null +++ b/core/java/android/credentials/CredentialDescription.aidl @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +/** + * @hide + */ +parcelable CredentialDescription; \ No newline at end of file diff --git a/core/java/android/credentials/CredentialDescription.java b/core/java/android/credentials/CredentialDescription.java new file mode 100644 index 0000000000000..b4310f2218d3c --- /dev/null +++ b/core/java/android/credentials/CredentialDescription.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; +import android.service.credentials.CredentialEntry; + +import com.android.internal.util.AnnotationValidations; +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Represents the type and contained data fields of a {@link Credential}. + * @hide + */ +public final class CredentialDescription implements Parcelable { + + /** + * The credential type. + */ + @NonNull + private final String mType; + + /** + * The flattened JSON string that will be matched with requests. + */ + @NonNull + private final String mFlattenedRequestString; + + /** + * The entry to be used in the UI. + */ + @NonNull + private final List mCredentialEntries; + + /** + * Constructs a {@link CredentialDescription}. + * + * @param type the type of the credential returned. + * @param flattenedRequestString flattened JSON string that will be matched with requests. + * @param credentialEntries a list of {@link CredentialEntry}s that have been returned + * to the developer upon credential creation. + * + * @throws IllegalArgumentException If type is empty. + */ + public CredentialDescription(@NonNull String type, + @NonNull String flattenedRequestString, + @NonNull List credentialEntries) { + mType = Preconditions.checkStringNotEmpty(type, "type must not be empty"); + mFlattenedRequestString = Preconditions.checkStringNotEmpty(flattenedRequestString); + mCredentialEntries = Objects.requireNonNull(credentialEntries); + } + + private CredentialDescription(@NonNull Parcel in) { + String type = in.readString8(); + String flattenedRequestString = in.readString(); + List entries = new ArrayList<>(); + in.readTypedList(entries, CredentialEntry.CREATOR); + + mType = type; + AnnotationValidations.validate(android.annotation.NonNull.class, null, mType); + mFlattenedRequestString = flattenedRequestString; + AnnotationValidations.validate(android.annotation.NonNull.class, null, + mFlattenedRequestString); + mCredentialEntries = entries; + AnnotationValidations.validate(android.annotation.NonNull.class, null, + mCredentialEntries); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public CredentialDescription createFromParcel(Parcel in) { + return new CredentialDescription(in); + } + + @Override + public CredentialDescription[] newArray(int size) { + return new CredentialDescription[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mType); + dest.writeString(mFlattenedRequestString); + dest.writeTypedList(mCredentialEntries, flags); + } + + @NonNull + public String getType() { + return mType; + } + + @NonNull + public String getFlattenedRequestString() { + return mFlattenedRequestString; + } + + @NonNull + public List getCredentialEntries() { + return mCredentialEntries; + } + + @Override + public int hashCode() { + return Objects.hash(mType, mFlattenedRequestString); + } + + @Override + public boolean equals(Object obj) { + return Objects.equals(mType, ((CredentialDescription) obj).getType()) + && Objects.equals(mFlattenedRequestString, ((CredentialDescription) obj).getType()); + } +} diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index 909d65483a1e6..e15cec84cbd0f 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -62,6 +62,14 @@ public final class CredentialManager { public static final String DEVICE_CONFIG_ENABLE_CREDENTIAL_MANAGER = "enable_credential_manager"; + /** + * Flag to enable and disable Credential Description api. + * + * @hide + */ + private static final String DEVICE_CONFIG_ENABLE_CREDENTIAL_DESC_API = + "enable_credential_description_api"; + /** * @hide instantiated by ContextImpl. */ @@ -294,6 +302,112 @@ public final class CredentialManager { true); } + /** + * Returns whether the credential description api is enabled. + * + * @hide + */ + public static boolean isCredentialDescriptionApiEnabled() { + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_CREDENTIAL, DEVICE_CONFIG_ENABLE_CREDENTIAL_DESC_API, false); + } + + /** + * Registers a {@link CredentialDescription} for an actively provisioned {@link Credential} + * a CredentialProvider has. This registry will then be used by + * {@link #executeGetCredential(GetCredentialRequest, Activity, + * CancellationSignal, Executor, OutcomeReceiver)} to determine where to + * fetch the requested {@link Credential} from. + * + * + * @param request the request data + * @param cancellationSignal an optional signal that allows for cancelling this call + * @param executor the callback will take place on this {@link Executor} + * @param callback the callback invoked when the request succeeds or fails + * + * @throws {@link UnsupportedOperationException} if the feature has not been enabled. + * + * @hide + */ + public void registerCredentialDescription( + @NonNull RegisterCredentialDescriptionRequest request, + @Nullable CancellationSignal cancellationSignal, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver callback) { + + if (!isCredentialDescriptionApiEnabled()) { + throw new UnsupportedOperationException("This API is not currently supported."); + } + + requireNonNull(executor, "executor must not be null"); + requireNonNull(callback, "callback must not be null"); + + if (cancellationSignal != null && cancellationSignal.isCanceled()) { + Log.w(TAG, "executeCreateCredential already canceled"); + return; + } + + ICancellationSignal cancelRemote = null; + try { + cancelRemote = mService.registerCredentialDescription(request, + new RegisterCredentialDescriptionTransport(executor, callback), + mContext.getOpPackageName()); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + if (cancellationSignal != null && cancelRemote != null) { + cancellationSignal.setRemote(cancelRemote); + } + } + + + /** + * Unregisters a {@link CredentialDescription} for an actively provisioned {@link Credential} + * that has been registered previously. + * + * + * @param request the request data + * @param cancellationSignal an optional signal that allows for cancelling this call + * @param executor the callback will take place on this {@link Executor} + * @param callback the callback invoked when the request succeeds or fails + * + * @throws {@link UnsupportedOperationException} if the feature has not been enabled. + * + * @hide + */ + public void unRegisterCredentialDescription( + @NonNull UnregisterCredentialDescriptionRequest request, + @Nullable CancellationSignal cancellationSignal, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver callback) { + + if (!isCredentialDescriptionApiEnabled()) { + throw new UnsupportedOperationException("This API is not currently supported."); + } + + requireNonNull(executor, "executor must not be null"); + requireNonNull(callback, "callback must not be null"); + + if (cancellationSignal != null && cancellationSignal.isCanceled()) { + Log.w(TAG, "executeCreateCredential already canceled"); + return; + } + + ICancellationSignal cancelRemote = null; + try { + cancelRemote = mService.unRegisterCredentialDescription(request, + new UnregisterCredentialDescriptionTransport(executor, callback), + mContext.getOpPackageName()); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + if (cancellationSignal != null && cancelRemote != null) { + cancellationSignal.setRemote(cancelRemote); + } + } + private static class GetCredentialTransport extends IGetCredentialCallback.Stub { // TODO: listen for cancellation to release callback. @@ -455,4 +569,54 @@ public final class CredentialManager { () -> mCallback.onError(new SetEnabledProvidersException(errorType, message))); } } + + private static class RegisterCredentialDescriptionTransport + extends IRegisterCredentialDescriptionCallback.Stub { + + private final Executor mExecutor; + private final OutcomeReceiver mCallback; + + private RegisterCredentialDescriptionTransport(Executor executor, + OutcomeReceiver callback) { + mExecutor = executor; + mCallback = callback; + } + + @Override + public void onResponse() { + mCallback.onResult(null); + } + + @Override + public void onError(String errorCode, String message) { + mExecutor.execute( + () -> mCallback.onError(new RegisterCredentialDescriptionException(errorCode, + message))); + } + } + + private static class UnregisterCredentialDescriptionTransport + extends IUnregisterCredentialDescriptionCallback.Stub { + + private final Executor mExecutor; + private final OutcomeReceiver mCallback; + + private UnregisterCredentialDescriptionTransport(Executor executor, + OutcomeReceiver callback) { + mExecutor = executor; + mCallback = callback; + } + + @Override + public void onResponse() { + mCallback.onResult(null); + } + + @Override + public void onError(String errorCode, String message) { + mExecutor.execute( + () -> mCallback.onError(new UnregisterCredentialDescriptionException(errorCode, + message))); + } + } } diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl index c3ca03dcdfd2c..75b3d0c78d230 100644 --- a/core/java/android/credentials/ICredentialManager.aidl +++ b/core/java/android/credentials/ICredentialManager.aidl @@ -21,10 +21,14 @@ import java.util.List; import android.credentials.ClearCredentialStateRequest; import android.credentials.CreateCredentialRequest; import android.credentials.GetCredentialRequest; +import android.credentials.RegisterCredentialDescriptionRequest; +import android.credentials.UnregisterCredentialDescriptionRequest; import android.credentials.IClearCredentialStateCallback; import android.credentials.ICreateCredentialCallback; import android.credentials.IGetCredentialCallback; import android.credentials.IListEnabledProvidersCallback; +import android.credentials.IRegisterCredentialDescriptionCallback; +import android.credentials.IUnregisterCredentialDescriptionCallback; import android.credentials.ISetEnabledProvidersCallback; import android.os.ICancellationSignal; @@ -44,4 +48,9 @@ interface ICredentialManager { @nullable ICancellationSignal listEnabledProviders(in IListEnabledProvidersCallback callback); void setEnabledProviders(in List providers, in int userId, in ISetEnabledProvidersCallback callback); + + @nullable ICancellationSignal registerCredentialDescription(in RegisterCredentialDescriptionRequest request, in IRegisterCredentialDescriptionCallback callback, String callingPackage); + + @nullable ICancellationSignal unRegisterCredentialDescription(in UnregisterCredentialDescriptionRequest request, in IUnregisterCredentialDescriptionCallback callback, String callingPackage); } + diff --git a/core/java/android/credentials/IRegisterCredentialDescriptionCallback.aidl b/core/java/android/credentials/IRegisterCredentialDescriptionCallback.aidl new file mode 100644 index 0000000000000..124a319e11d82 --- /dev/null +++ b/core/java/android/credentials/IRegisterCredentialDescriptionCallback.aidl @@ -0,0 +1,27 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +/** + * Listener for an registerCredentialDescription request. + * + * @hide + */ +interface IRegisterCredentialDescriptionCallback { + oneway void onResponse(); + oneway void onError(String errorCode, String message); +} \ No newline at end of file diff --git a/core/java/android/credentials/IUnregisterCredentialDescriptionCallback.aidl b/core/java/android/credentials/IUnregisterCredentialDescriptionCallback.aidl new file mode 100644 index 0000000000000..b30a12a5cd377 --- /dev/null +++ b/core/java/android/credentials/IUnregisterCredentialDescriptionCallback.aidl @@ -0,0 +1,27 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +/** + * Listener for an registerCredentialDescription request. + * + * @hide + */ +interface IUnregisterCredentialDescriptionCallback { + oneway void onResponse(); + oneway void onError(String errorCode, String message); +} \ No newline at end of file diff --git a/core/java/android/credentials/RegisterCredentialDescriptionException.java b/core/java/android/credentials/RegisterCredentialDescriptionException.java new file mode 100644 index 0000000000000..3cf5a752495fd --- /dev/null +++ b/core/java/android/credentials/RegisterCredentialDescriptionException.java @@ -0,0 +1,80 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.CancellationSignal; +import android.os.OutcomeReceiver; + +import com.android.internal.util.Preconditions; + +import java.util.concurrent.Executor; + +/** + * Represents an error encountered during the {@link + * CredentialManager#registerCredentialDescription(RegisterCredentialDescriptionRequest, + * CancellationSignal, Executor, OutcomeReceiver)} operation. + * + * @hide + */ +public class RegisterCredentialDescriptionException extends Exception { + + @NonNull public final String errorType; + + /** + * Constructs a {@link RegisterCredentialDescriptionException}. + * + * @throws IllegalArgumentException If errorType is empty. + */ + public RegisterCredentialDescriptionException(@NonNull String errorType, + @Nullable String message) { + this(errorType, message, null); + } + + /** + * Constructs a {@link RegisterCredentialDescriptionException}. + * + * @throws IllegalArgumentException If errorType is empty. + */ + public RegisterCredentialDescriptionException( + @NonNull String errorType, @Nullable String message, @Nullable Throwable cause) { + super(message, cause); + this.errorType = + Preconditions + .checkStringNotEmpty(errorType, "errorType must not be empty"); + } + + /** + * Constructs a {@link RegisterCredentialDescriptionException}. + * + * @throws IllegalArgumentException If errorType is empty. + */ + public RegisterCredentialDescriptionException(@NonNull String errorType, + @Nullable Throwable cause) { + this(errorType, null, cause); + } + + /** + * Constructs a {@link RegisterCredentialDescriptionException}. + * + * @throws IllegalArgumentException If errorType is empty. + */ + public RegisterCredentialDescriptionException(@NonNull String errorType) { + this(errorType, null, null); + } +} diff --git a/core/java/android/credentials/RegisterCredentialDescriptionRequest.aidl b/core/java/android/credentials/RegisterCredentialDescriptionRequest.aidl new file mode 100644 index 0000000000000..1d567282f447f --- /dev/null +++ b/core/java/android/credentials/RegisterCredentialDescriptionRequest.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +parcelable RegisterCredentialDescriptionRequest; \ No newline at end of file diff --git a/core/java/android/credentials/RegisterCredentialDescriptionRequest.java b/core/java/android/credentials/RegisterCredentialDescriptionRequest.java new file mode 100644 index 0000000000000..de312797c5ac9 --- /dev/null +++ b/core/java/android/credentials/RegisterCredentialDescriptionRequest.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.content.ComponentName; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + + +/** + * A request to register a {@link ComponentName} that contains an actively provisioned + * {@link Credential} represented by a {@link CredentialDescription}. + * + * @hide + */ +public final class RegisterCredentialDescriptionRequest implements Parcelable { + + public static final String FLATTENED_REQUEST_STRING_KEY = "flattened_request_string"; + + @NonNull + private final List mCredentialDescriptions; + + public RegisterCredentialDescriptionRequest( + @NonNull CredentialDescription credentialDescription) { + mCredentialDescriptions = Arrays.asList(requireNonNull(credentialDescription)); + } + + public RegisterCredentialDescriptionRequest( + @NonNull List credentialDescriptions) { + mCredentialDescriptions = new ArrayList<>(requireNonNull(credentialDescriptions)); + } + + private RegisterCredentialDescriptionRequest(@NonNull Parcel in) { + List credentialDescriptions = new ArrayList<>(); + in.readTypedList(credentialDescriptions, CredentialDescription.CREATOR); + + mCredentialDescriptions = new ArrayList<>(); + AnnotationValidations.validate(android.annotation.NonNull.class, null, + credentialDescriptions); + mCredentialDescriptions.addAll(credentialDescriptions); + } + + public static final @NonNull Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public RegisterCredentialDescriptionRequest createFromParcel(Parcel in) { + return new RegisterCredentialDescriptionRequest(in); + } + + @Override + public RegisterCredentialDescriptionRequest[] newArray(int size) { + return new RegisterCredentialDescriptionRequest[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeTypedList(mCredentialDescriptions, flags); + } + + @NonNull + public List getCredentialDescriptions() { + return mCredentialDescriptions; + } +} diff --git a/core/java/android/credentials/UnregisterCredentialDescriptionException.java b/core/java/android/credentials/UnregisterCredentialDescriptionException.java new file mode 100644 index 0000000000000..0c786bda54a40 --- /dev/null +++ b/core/java/android/credentials/UnregisterCredentialDescriptionException.java @@ -0,0 +1,80 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.CancellationSignal; +import android.os.OutcomeReceiver; + +import com.android.internal.util.Preconditions; + +import java.util.concurrent.Executor; + +/** + * Represents an error encountered during the {@link + * CredentialManager#registerCredentialDescription(RegisterCredentialDescriptionRequest, + * CancellationSignal, Executor, OutcomeReceiver)} operation. + * + * @hide + */ +public class UnregisterCredentialDescriptionException extends Exception { + + @NonNull public final String errorType; + + /** + * Constructs a {@link RegisterCredentialDescriptionException}. + * + * @throws IllegalArgumentException If errorType is empty. + */ + public UnregisterCredentialDescriptionException(@NonNull String errorType, + @Nullable String message) { + this(errorType, message, null); + } + + /** + * Constructs a {@link RegisterCredentialDescriptionException}. + * + * @throws IllegalArgumentException If errorType is empty. + */ + public UnregisterCredentialDescriptionException( + @NonNull String errorType, @Nullable String message, @Nullable Throwable cause) { + super(message, cause); + this.errorType = + Preconditions + .checkStringNotEmpty(errorType, "errorType must not be empty"); + } + + /** + * Constructs a {@link RegisterCredentialDescriptionException}. + * + * @throws IllegalArgumentException If errorType is empty. + */ + public UnregisterCredentialDescriptionException(@NonNull String errorType, + @Nullable Throwable cause) { + this(errorType, null, cause); + } + + /** + * Constructs a {@link RegisterCredentialDescriptionException}. + * + * @throws IllegalArgumentException If errorType is empty. + */ + public UnregisterCredentialDescriptionException(@NonNull String errorType) { + this(errorType, null, null); + } +} diff --git a/core/java/android/credentials/UnregisterCredentialDescriptionRequest.aidl b/core/java/android/credentials/UnregisterCredentialDescriptionRequest.aidl new file mode 100644 index 0000000000000..e25f13c0c6231 --- /dev/null +++ b/core/java/android/credentials/UnregisterCredentialDescriptionRequest.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +parcelable UnregisterCredentialDescriptionRequest; \ No newline at end of file diff --git a/core/java/android/credentials/UnregisterCredentialDescriptionRequest.java b/core/java/android/credentials/UnregisterCredentialDescriptionRequest.java new file mode 100644 index 0000000000000..f3454c101d9cf --- /dev/null +++ b/core/java/android/credentials/UnregisterCredentialDescriptionRequest.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.content.ComponentName; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; + + +/** + * A request to unregister a {@link ComponentName} that contains an actively provisioned + * {@link Credential} represented by a {@link CredentialDescription}. * + * + * @hide + */ +public final class UnregisterCredentialDescriptionRequest implements Parcelable { + + @NonNull + private final CredentialDescription mCredentialDescription; + + public UnregisterCredentialDescriptionRequest(@NonNull CredentialDescription + credentialDescription) { + mCredentialDescription = requireNonNull(credentialDescription); + } + + private UnregisterCredentialDescriptionRequest(@NonNull Parcel in) { + CredentialDescription credentialDescription = + CredentialDescription.CREATOR.createFromParcel(in); + + mCredentialDescription = credentialDescription; + AnnotationValidations.validate(android.annotation.NonNull.class, null, + credentialDescription); + } + + public static final @NonNull Parcelable.Creator + CREATOR = new Parcelable.Creator() { + @Override + public UnregisterCredentialDescriptionRequest createFromParcel(Parcel in) { + return new UnregisterCredentialDescriptionRequest(in); + } + + @Override + public UnregisterCredentialDescriptionRequest[] newArray(int size) { + return new UnregisterCredentialDescriptionRequest[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + mCredentialDescription.writeToParcel(dest, flags); + } + + @NonNull + public CredentialDescription getCredentialDescription() { + return mCredentialDescription; + } +} diff --git a/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java b/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java new file mode 100644 index 0000000000000..b7c5fc2de4f31 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/CredentialDescriptionRegistry.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.credentials; + +import android.credentials.CredentialDescription; +import android.credentials.IRegisterCredentialDescriptionCallback; +import android.credentials.IUnregisterCredentialDescriptionCallback; +import android.credentials.RegisterCredentialDescriptionRequest; +import android.credentials.UnregisterCredentialDescriptionRequest; +import android.os.RemoteException; +import android.util.SparseArray; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** Contains information on what CredentialProvider has what provisioned Credential. */ +public class CredentialDescriptionRegistry { + + private static final int MAX_ALLOWED_CREDENTIAL_DESCRIPTIONS = 128; + private static SparseArray sCredentialDescriptionSessionPerUser; + + static { + sCredentialDescriptionSessionPerUser = new SparseArray<>(); + } + + // TODO(b/265992655): add a way to update CredentialRegistry when a user is removed. + /** Get and/or create a {@link CredentialDescription} for the given user id. */ + public static CredentialDescriptionRegistry forUser(int userId) { + CredentialDescriptionRegistry session = + sCredentialDescriptionSessionPerUser.get(userId, null); + + if (session == null) { + session = new CredentialDescriptionRegistry(); + sCredentialDescriptionSessionPerUser.put(userId, session); + } + return session; + } + + private Map> mCredentialDescriptions; + + private CredentialDescriptionRegistry() { + this.mCredentialDescriptions = new HashMap<>(); + } + + /** Handle the given {@link RegisterCredentialDescriptionRequest} by creating + * the appropriate package name mapping. */ + public void executeRegisterRequest(RegisterCredentialDescriptionRequest request, + String callingPackageName, + IRegisterCredentialDescriptionCallback callback) { + + if (!mCredentialDescriptions.containsKey(callingPackageName) + && mCredentialDescriptions.size() <= MAX_ALLOWED_CREDENTIAL_DESCRIPTIONS) { + mCredentialDescriptions.put(callingPackageName, new HashSet<>()); + } + + mCredentialDescriptions.get(callingPackageName) + .addAll(request.getCredentialDescriptions()); + + try { + callback.onResponse(); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + /** Handle the given {@link UnregisterCredentialDescriptionRequest} by creating + * the appropriate package name mapping. */ + public void executeUnregisterRequest( + UnregisterCredentialDescriptionRequest request, + String callingPackageName, + IUnregisterCredentialDescriptionCallback callback) { + + if (mCredentialDescriptions.containsKey(callingPackageName)) { + mCredentialDescriptions.get(callingPackageName) + .remove(request.getCredentialDescription()); + } + + try { + callback.onResponse(); + } catch (RemoteException e) { + e.printStackTrace(); + } + } + + /** Returns package names of CredentialProviders that can satisfy a given + * {@link CredentialDescription}. */ + public Set filterCredentials(String flatRequestString) { + + Set result = new HashSet<>(); + + for (String componentName: mCredentialDescriptions.keySet()) { + Set currentSet = mCredentialDescriptions.get(componentName); + for (CredentialDescription containedDescription: currentSet) { + if (flatRequestString.equals(containedDescription.getFlattenedRequestString())) { + result.add(componentName); + } + } + } + + return result; + } + + void evictProviderWithPackageName(String packageName) { + if (mCredentialDescriptions.containsKey(packageName)) { + mCredentialDescriptions.remove(packageName); + } + } + +} diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index f76cf4993ebce..620b81bd0cd9c 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -36,14 +36,19 @@ import android.credentials.ICreateCredentialCallback; import android.credentials.ICredentialManager; import android.credentials.IGetCredentialCallback; import android.credentials.IListEnabledProvidersCallback; +import android.credentials.IRegisterCredentialDescriptionCallback; import android.credentials.ISetEnabledProvidersCallback; +import android.credentials.IUnregisterCredentialDescriptionCallback; import android.credentials.ListEnabledProvidersResponse; +import android.credentials.RegisterCredentialDescriptionRequest; +import android.credentials.UnregisterCredentialDescriptionRequest; import android.credentials.ui.IntentFactory; import android.os.Binder; import android.os.CancellationSignal; import android.os.ICancellationSignal; import android.os.RemoteException; import android.os.UserHandle; +import android.provider.DeviceConfig; import android.provider.Settings; import android.service.credentials.BeginCreateCredentialRequest; import android.service.credentials.BeginGetCredentialRequest; @@ -59,9 +64,13 @@ import com.android.server.infra.AbstractMasterSystemService; import com.android.server.infra.SecureSettingsServiceNameResolver; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Entry point service for credential management. @@ -75,6 +84,8 @@ public final class CredentialManagerService CredentialManagerService, CredentialManagerServiceImpl> { private static final String TAG = "CredManSysService"; + private static final String DEVICE_CONFIG_ENABLE_CREDENTIAL_DESC_API = + "enable_credential_description_api"; private final Context mContext; @@ -164,6 +175,7 @@ public final class CredentialManagerService if (services == null) { return; } + CredentialManagerServiceImpl serviceToBeRemoved = null; for (CredentialManagerServiceImpl service : services) { if (service != null) { @@ -180,10 +192,14 @@ public final class CredentialManagerService } if (serviceToBeRemoved != null) { removeServiceFromCache(serviceToBeRemoved, userId); + CredentialDescriptionRegistry.forUser(userId) + .evictProviderWithPackageName(serviceToBeRemoved.getServicePackageName()); } // TODO("Iterate over system services and remove if needed") } + + @GuardedBy("mLock") private List getOrConstructSystemServiceListLock( int resolvedUserId) { @@ -223,6 +239,53 @@ public final class CredentialManagerService concatenatedServices.addAll(getOrConstructSystemServiceListLock(userId)); return concatenatedServices; } + public static boolean isCredentialDescriptionApiEnabled() { + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_CREDENTIAL, DEVICE_CONFIG_ENABLE_CREDENTIAL_DESC_API, false); + } + + @SuppressWarnings("GuardedBy") // ErrorProne requires initiateProviderSessionForRequestLocked + // to be guarded by 'service.mLock', which is the same as mLock. + private List initiateProviderSessionsWithActiveContainers( + RequestSession session, + List requestOptions, Set activeCredentialContainers) { + List providerSessions = new ArrayList<>(); + // Invoke all services of a user to initiate a provider session + runForUser((service) -> { + if (activeCredentialContainers.contains(service.getComponentName())) { + ProviderSession providerSession = service + .initiateProviderSessionForRequestLocked(session, requestOptions); + if (providerSession != null) { + providerSessions.add(providerSession); + } + } + }); + return providerSessions; + } + + @NonNull + private Set getMatchingProviders(GetCredentialRequest request) { + // Session for active/provisioned credential descriptions; + CredentialDescriptionRegistry registry = CredentialDescriptionRegistry + .forUser(UserHandle.getCallingUserId()); + + // All requested credential descriptions based on the given request. + Set requestedCredentialDescriptions = + request.getGetCredentialOptions().stream().map( + getCredentialOption -> getCredentialOption + .getCredentialRetrievalData() + .getString(RegisterCredentialDescriptionRequest + .FLATTENED_REQUEST_STRING_KEY)) + .collect(Collectors.toSet()); + + // All requested credential descriptions based on the given request. + return requestedCredentialDescriptions.stream() + .map(registry::filterCredentials) + .flatMap( + (Function, Stream>) + Collection::stream) + .collect(Collectors.toSet()); + } @SuppressWarnings("GuardedBy") // ErrorProne requires initiateProviderSessionForRequestLocked // to be guarded by 'service.mLock', which is the same as mLock. @@ -282,11 +345,11 @@ public final class CredentialManagerService // Initiate all provider sessions List providerSessions = - initiateProviderSessions( - session, - request.getGetCredentialOptions().stream() - .map(GetCredentialOption::getType) - .collect(Collectors.toList())); + initiateProviderSessions( + session, + request.getGetCredentialOptions().stream() + .map(GetCredentialOption::getType) + .collect(Collectors.toList())); if (providerSessions.isEmpty()) { try { @@ -316,7 +379,7 @@ public final class CredentialManagerService ICreateCredentialCallback callback, String callingPackage) { Log.i(TAG, "starting executeCreateCredential with callingPackage: " + callingPackage); - // TODO : Implement cancellation + ICancellationSignal cancelTransport = CancellationSignal.createTransport(); // New request session, scoped for this request only. @@ -478,5 +541,77 @@ public final class CredentialManagerService }); return cancelTransport; } + + @Override + public ICancellationSignal registerCredentialDescription( + RegisterCredentialDescriptionRequest request, + IRegisterCredentialDescriptionCallback callback, String callingPackage) { + Log.i(TAG, "registerCredentialDescription"); + ICancellationSignal cancelTransport = CancellationSignal.createTransport(); + + + List services = + CredentialProviderInfo.getAvailableServices(mContext, + UserHandle.getCallingUserId()); + + List providers = services.stream() + .map(credentialProviderInfo + -> credentialProviderInfo.getServiceInfo().packageName).toList(); + if (!providers.contains(callingPackage)) { + try { + callback.onError("UNKNOWN", + "Not an existing provider."); + } catch (RemoteException e) { + Log.i( + TAG, + "Issue invoking onError on IRegisterCredentialDescriptionCallback " + + "callback: " + + e.getMessage()); + } + } + + CredentialDescriptionRegistry session = CredentialDescriptionRegistry + .forUser(UserHandle.getCallingUserId()); + + session.executeRegisterRequest(request, callingPackage, callback); + + return cancelTransport; + } + + @Override + public ICancellationSignal unRegisterCredentialDescription( + UnregisterCredentialDescriptionRequest request, + IUnregisterCredentialDescriptionCallback callback, String callingPackage) { + Log.i(TAG, "registerCredentialDescription"); + ICancellationSignal cancelTransport = CancellationSignal.createTransport(); + + List services = + CredentialProviderInfo.getAvailableServices(mContext, + UserHandle.getCallingUserId()); + + List providers = services.stream() + .map(credentialProviderInfo + -> credentialProviderInfo.getServiceInfo().packageName).toList(); + + if (!providers.contains(callingPackage)) { + try { + callback.onError("UNKNOWN", + "Not an existing provider."); + } catch (RemoteException e) { + Log.i( + TAG, + "Issue invoking onError on IRegisterCredentialDescriptionCallback " + + "callback: " + + e.getMessage()); + } + } + + CredentialDescriptionRegistry session = CredentialDescriptionRegistry + .forUser(UserHandle.getCallingUserId()); + + session.executeUnregisterRequest(request, callingPackage, callback); + + return cancelTransport; + } } }