From eff614db0097971038e1cf0d6180b3b5d9a589cd Mon Sep 17 00:00:00 2001 From: Alex Johnston Date: Mon, 16 Nov 2020 14:24:52 +0000 Subject: Add credential management app to platform - This is part of the work to support a credential management app on unmanaged devices. - Add intent and method in KeyChain to allow an app to request to become the credential management app. - Add the class CredentialManagementApp to store the current credential management app. - Add the class AppUriAuthenticationPolicy and an extra in KeyChain to allow an app to set an authentication policy. - Add API methods to KeyChainService to set, get and retrieve the credential management app. Bug: 165641221 Test: atest CredentialManagementAppTest atest AppUriAuthenticationPolicyTest adb shell am start -n com.android.keychain.tests/.KeyChainTestActivity Change-Id: I1e57ed9c18a1ada463c55dbf17ce30e31aa7bad2 --- .../security/AppUriAuthenticationPolicy.aidl | 19 ++ .../security/AppUriAuthenticationPolicy.java | 226 +++++++++++++++++++++ .../android/security/CredentialManagementApp.java | 123 +++++++++++ keystore/java/android/security/Credentials.java | 2 + keystore/java/android/security/KeyChain.java | 38 ++++ keystore/java/android/security/UrisToAliases.java | 138 +++++++++++++ 6 files changed, 546 insertions(+) create mode 100644 keystore/java/android/security/AppUriAuthenticationPolicy.aidl create mode 100644 keystore/java/android/security/AppUriAuthenticationPolicy.java create mode 100644 keystore/java/android/security/CredentialManagementApp.java create mode 100644 keystore/java/android/security/UrisToAliases.java (limited to 'keystore/java') diff --git a/keystore/java/android/security/AppUriAuthenticationPolicy.aidl b/keystore/java/android/security/AppUriAuthenticationPolicy.aidl new file mode 100644 index 000000000000..5c52c86f0426 --- /dev/null +++ b/keystore/java/android/security/AppUriAuthenticationPolicy.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 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.security; + +parcelable AppUriAuthenticationPolicy; diff --git a/keystore/java/android/security/AppUriAuthenticationPolicy.java b/keystore/java/android/security/AppUriAuthenticationPolicy.java new file mode 100644 index 000000000000..30f5a94ca0c8 --- /dev/null +++ b/keystore/java/android/security/AppUriAuthenticationPolicy.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2020 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.security; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * The app-URI authentication policy is set by the credential management app. This policy determines + * which alias for a private key and certificate pair should be used for authentication. + *

+ * The authentication policy should be added as a parameter when calling + * {@link KeyChain#createManageCredentialsIntent}. + *

+ * Example: + *

{@code
+ *     AppUriAuthenticationPolicy authenticationPolicy = new AppUriAuthenticationPolicy.Builder()
+ *              .addAppAndUriMapping("com.test.pkg", testUri, "testAlias")
+ *              .addAppAndUriMapping("com.test2.pkg", testUri1, "testAlias2")
+ *              .addAppAndUriMapping("com.test2.pkg", testUri2, "testAlias2")
+ *              .build();
+ *     Intent requestIntent = KeyChain.createManageCredentialsIntent(authenticationPolicy);
+ * }
+ *

+ */ +public final class AppUriAuthenticationPolicy implements Parcelable { + + private static final String KEY_AUTHENTICATION_POLICY_APP_TO_URIS = + "authentication_policy_app_to_uris"; + private static final String KEY_AUTHENTICATION_POLICY_APP = "policy_app"; + + /** + * The mappings from an app and list of URIs to a list of aliases, which will be used for + * authentication. + *

+ * appPackageName -> uri -> alias + */ + @NonNull + private final Map mAppToUris; + + private AppUriAuthenticationPolicy(@NonNull Map appToUris) { + Objects.requireNonNull(appToUris); + this.mAppToUris = appToUris; + } + + /** + * Builder class for {@link AppUriAuthenticationPolicy} objects. + */ + public static final class Builder { + private Map mPackageNameToUris; + + /** + * Initialize a new Builder to construct an {@link AppUriAuthenticationPolicy}. + */ + public Builder() { + mPackageNameToUris = new HashMap<>(); + } + + /** + * Adds mappings from an app and URI to an alias, which will be used for authentication. + *

+ * If this method is called with a package name and URI that was previously added, the + * previous alias will be overwritten. + * + * @param appPackageName The app's package name to authenticate the user to. + * @param uri The URI to authenticate the user to. + * @param alias The alias which will be used for authentication. + * + * @return the same Builder instance. + */ + @NonNull + public Builder addAppAndUriMapping(@NonNull String appPackageName, @NonNull Uri uri, + @NonNull String alias) { + Objects.requireNonNull(appPackageName); + Objects.requireNonNull(uri); + Objects.requireNonNull(alias); + UrisToAliases urisToAliases = + mPackageNameToUris.getOrDefault(appPackageName, new UrisToAliases()); + urisToAliases.addUriToAlias(uri, alias); + mPackageNameToUris.put(appPackageName, urisToAliases); + return this; + } + + /** + * Adds mappings from an app and list of URIs to a list of aliases, which will be used for + * authentication. + *

+ * appPackageName -> uri -> alias + * + * @hide + */ + @NonNull + public Builder addAppAndUriMapping(@NonNull String appPackageName, + @NonNull UrisToAliases urisToAliases) { + Objects.requireNonNull(appPackageName); + Objects.requireNonNull(urisToAliases); + mPackageNameToUris.put(appPackageName, urisToAliases); + return this; + } + + /** + * Combines all of the attributes that have been set on the {@link Builder} + * + * @return a new {@link AppUriAuthenticationPolicy} object. + */ + @NonNull + public AppUriAuthenticationPolicy build() { + return new AppUriAuthenticationPolicy(mPackageNameToUris); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeMap(mAppToUris); + } + + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public AppUriAuthenticationPolicy createFromParcel(Parcel in) { + Map appToUris = new HashMap<>(); + in.readMap(appToUris, UrisToAliases.class.getClassLoader()); + return new AppUriAuthenticationPolicy(appToUris); + } + + @Override + public AppUriAuthenticationPolicy[] newArray(int size) { + return new AppUriAuthenticationPolicy[size]; + } + }; + + @Override + public String toString() { + return "AppUriAuthenticationPolicy{" + + "mPackageNameToUris=" + mAppToUris + + '}'; + } + + /** + * Return the authentication policy mapping, which determines which alias for a private key + * and certificate pair should be used for authentication. + *

+ * appPackageName -> uri -> alias + */ + @NonNull + public Map> getAppAndUriMappings() { + Map> appAndUris = new HashMap<>(); + for (Map.Entry entry : mAppToUris.entrySet()) { + appAndUris.put(entry.getKey(), entry.getValue().getUrisToAliases()); + } + return appAndUris; + } + + /** + * Restore a previously saved {@link AppUriAuthenticationPolicy} from XML. + * + * @hide + */ + @Nullable + public static AppUriAuthenticationPolicy readFromXml(@NonNull XmlPullParser parser) + throws IOException, XmlPullParserException { + AppUriAuthenticationPolicy.Builder builder = new AppUriAuthenticationPolicy.Builder(); + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + if (!parser.getName().equals(KEY_AUTHENTICATION_POLICY_APP_TO_URIS)) { + continue; + } + String app = parser.getAttributeValue(null, KEY_AUTHENTICATION_POLICY_APP); + UrisToAliases urisToAliases = UrisToAliases.readFromXml(parser); + builder.addAppAndUriMapping(app, urisToAliases); + } + return builder.build(); + } + + /** + * Save the {@link AppUriAuthenticationPolicy} to XML. + * + * @hide + */ + public void writeToXml(@NonNull XmlSerializer out) throws IOException { + for (Map.Entry appsToUris : mAppToUris.entrySet()) { + out.startTag(null, KEY_AUTHENTICATION_POLICY_APP_TO_URIS); + out.attribute(null, KEY_AUTHENTICATION_POLICY_APP, appsToUris.getKey()); + appsToUris.getValue().writeToXml(out); + out.endTag(null, KEY_AUTHENTICATION_POLICY_APP_TO_URIS); + } + } + +} diff --git a/keystore/java/android/security/CredentialManagementApp.java b/keystore/java/android/security/CredentialManagementApp.java new file mode 100644 index 000000000000..cbb23015dbe8 --- /dev/null +++ b/keystore/java/android/security/CredentialManagementApp.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2020 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.security; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.Log; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.Objects; + +/** + * The credential management app has the ability to manage the user's KeyChain credentials on + * unmanaged devices. {@link KeyChain#createManageCredentialsIntent} should be used by an app to + * request to become the credential management app. The user must approve this request before the + * app can manage the user's credentials. + *

+ * Note: there can only be one credential management on the device. If another app requests to + * become the credential management app and the user approves, then the existing credential + * management app will no longer be able to manage credentials. + *

+ * The requesting credential management app should include its authentication policy in the + * requesting intent. The authentication policy declares which certificates should be used for a + * given list of apps and URIs. + * + * @hide + * @see AppUriAuthenticationPolicy + */ +public class CredentialManagementApp { + + private static final String TAG = "CredentialManagementApp"; + private static final String KEY_PACKAGE_NAME = "package_name"; + + /** + * The credential management app's package name + */ + @NonNull + private final String mPackageName; + + /** + * The mappings from an app and list of URIs to a list of aliases, which will be used for + * authentication. + *

+ * appPackageName -> uri -> alias + */ + @NonNull + private AppUriAuthenticationPolicy mAuthenticationPolicy; + + public CredentialManagementApp(@NonNull String packageName, + @NonNull AppUriAuthenticationPolicy authenticationPolicy) { + Objects.requireNonNull(packageName); + Objects.requireNonNull(authenticationPolicy); + mPackageName = packageName; + mAuthenticationPolicy = authenticationPolicy; + } + + /** + * Returns the package name of the credential management app. + */ + @NonNull + public String getPackageName() { + return mPackageName; + } + + /** + * Returns the authentication policy of the credential management app. + */ + @NonNull + public AppUriAuthenticationPolicy getAuthenticationPolicy() { + return mAuthenticationPolicy; + } + + /** + * Sets the authentication policy of the credential management app. + */ + public void setAuthenticationPolicy(@Nullable AppUriAuthenticationPolicy authenticationPolicy) { + Objects.requireNonNull(authenticationPolicy); + mAuthenticationPolicy = authenticationPolicy; + } + + /** + * Restore a previously saved {@link CredentialManagementApp} from XML. + */ + @Nullable + public static CredentialManagementApp readFromXml(@NonNull XmlPullParser parser) { + try { + String packageName = parser.getAttributeValue(null, KEY_PACKAGE_NAME); + AppUriAuthenticationPolicy policy = AppUriAuthenticationPolicy.readFromXml(parser); + return new CredentialManagementApp(packageName, policy); + } catch (XmlPullParserException | IOException e) { + Log.w(TAG, "Reading from xml failed", e); + } + return null; + } + + /** + * Save the {@link CredentialManagementApp} to XML. + */ + public void writeToXml(@NonNull XmlSerializer out) throws IOException { + out.attribute(null, KEY_PACKAGE_NAME, mPackageName); + if (mAuthenticationPolicy != null) { + mAuthenticationPolicy.writeToXml(out); + } + } +} diff --git a/keystore/java/android/security/Credentials.java b/keystore/java/android/security/Credentials.java index 7abcfdc98bc6..f41b6081e38c 100644 --- a/keystore/java/android/security/Credentials.java +++ b/keystore/java/android/security/Credentials.java @@ -49,6 +49,8 @@ public class Credentials { public static final String INSTALL_AS_USER_ACTION = "android.credentials.INSTALL_AS_USER"; + public static final String ACTION_MANAGE_CREDENTIALS = "android.security.MANAGE_CREDENTIALS"; + /** * Key prefix for CA certificates. * diff --git a/keystore/java/android/security/KeyChain.java b/keystore/java/android/security/KeyChain.java index a77aec2788af..c6e72b0e9f6e 100644 --- a/keystore/java/android/security/KeyChain.java +++ b/keystore/java/android/security/KeyChain.java @@ -15,6 +15,8 @@ */ package android.security; +import static android.security.Credentials.ACTION_MANAGE_CREDENTIALS; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SdkConstant; @@ -121,6 +123,11 @@ public final class KeyChain { */ private static final String CERT_INSTALLER_PACKAGE = "com.android.certinstaller"; + /** + * Package name for Settings. + */ + private static final String SETTINGS_PACKAGE = "com.android.settings"; + /** * Extra for use with {@link #ACTION_CHOOSER} * @hide Also used by KeyChainActivity implementation @@ -201,6 +208,20 @@ public final class KeyChain { // Compatible with old android.security.Credentials.PKCS12 public static final String EXTRA_PKCS12 = "PKCS12"; + /** + * Extra used by {@link #createManageCredentialsIntent(AppUriAuthenticationPolicy)} to specify + * the authentication policy of the credential management app. + * + *

The authentication policy declares which alias for a private key and certificate pair + * should be used for authentication, given a list of apps and URIs. + * + *

The extra value should be a {@link AppUriAuthenticationPolicy}. + * + * @hide + */ + public static final String EXTRA_AUTHENTICATION_POLICY = + "android.security.extra.AUTHENTICATION_POLICY"; + /** * Broadcast Action: Indicates the trusted storage has changed. Sent when * one of this happens: @@ -385,6 +406,23 @@ public final class KeyChain { return intent; } + /** + * Returns an {@code Intent} that should be used by an app to request to manage the user's + * credentials. This is limited to unmanaged devices. The authentication policy must be + * provided to be able to make this request successfully. + * + * @param policy The authentication policy determines which alias for a private key and + * certificate pair should be used for authentication. + */ + @NonNull + public static Intent createManageCredentialsIntent(@NonNull AppUriAuthenticationPolicy policy) { + Intent intent = new Intent(ACTION_MANAGE_CREDENTIALS); + intent.setComponent(ComponentName.createRelative(SETTINGS_PACKAGE, + ".security.RequestManageCredentials")); + intent.putExtra(EXTRA_AUTHENTICATION_POLICY, policy); + return intent; + } + /** * Launches an {@code Activity} for the user to select the alias * for a private key and certificate pair for authentication. The diff --git a/keystore/java/android/security/UrisToAliases.java b/keystore/java/android/security/UrisToAliases.java new file mode 100644 index 000000000000..65d433abe166 --- /dev/null +++ b/keystore/java/android/security/UrisToAliases.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2020 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.security; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * The mapping from URI to alias, which determines the alias to use when the user visits a URI. + * This mapping is part of the {@link AppUriAuthenticationPolicy}, which specifies which app this + * mapping should be used for. + * + * @hide + * @see AppUriAuthenticationPolicy + */ +public final class UrisToAliases implements Parcelable { + + private static final String KEY_AUTHENTICATION_POLICY_URI_TO_ALIAS = + "authentication_policy_uri_to_alias"; + private static final String KEY_AUTHENTICATION_POLICY_URI = "policy_uri"; + private static final String KEY_AUTHENTICATION_POLICY_ALIAS = "policy_alias"; + + /** + * The mappings from URIs to aliases, which will be used for authentication. + */ + @NonNull + private final Map mUrisToAliases; + + public UrisToAliases() { + this.mUrisToAliases = new HashMap<>(); + } + + private UrisToAliases(@NonNull Map urisToAliases) { + this.mUrisToAliases = urisToAliases; + } + + @NonNull + public static final Creator CREATOR = new Creator() { + @Override + public UrisToAliases createFromParcel(Parcel in) { + Map urisToAliases = new HashMap<>(); + in.readMap(urisToAliases, String.class.getClassLoader()); + return new UrisToAliases(urisToAliases); + } + + @Override + public UrisToAliases[] newArray(int size) { + return new UrisToAliases[size]; + } + }; + + /** + * Returns the mapping from URIs to aliases. + */ + @NonNull + public Map getUrisToAliases() { + return Collections.unmodifiableMap(mUrisToAliases); + } + + /** + * Adds mapping from an URI to an alias. + */ + public void addUriToAlias(@NonNull Uri uri, @NonNull String alias) { + mUrisToAliases.put(uri, alias); + } + + /** + * Restore a previously saved {@link UrisToAliases} from XML. + */ + @Nullable + public static UrisToAliases readFromXml(@NonNull XmlPullParser parser) + throws IOException, XmlPullParserException { + Map urisToAliases = new HashMap<>(); + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + if (!parser.getName().equals(KEY_AUTHENTICATION_POLICY_URI_TO_ALIAS)) { + continue; + } + Uri uri = Uri.parse(parser.getAttributeValue(null, KEY_AUTHENTICATION_POLICY_URI)); + String alias = parser.getAttributeValue(null, KEY_AUTHENTICATION_POLICY_ALIAS); + urisToAliases.put(uri, alias); + } + return new UrisToAliases(urisToAliases); + } + + /** + * Save the {@link UrisToAliases} to XML. + */ + public void writeToXml(@NonNull XmlSerializer out) throws IOException { + for (Map.Entry urisToAliases : mUrisToAliases.entrySet()) { + out.startTag(null, KEY_AUTHENTICATION_POLICY_URI_TO_ALIAS); + out.attribute(null, KEY_AUTHENTICATION_POLICY_URI, urisToAliases.getKey().toString()); + out.attribute(null, KEY_AUTHENTICATION_POLICY_ALIAS, urisToAliases.getValue()); + out.endTag(null, KEY_AUTHENTICATION_POLICY_URI_TO_ALIAS); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeMap(mUrisToAliases); + } +} -- cgit v1.2.3-59-g8ed1b