diff options
author | Alex Johnston <acjohnston@google.com> | 2020-11-16 14:24:52 +0000 |
---|---|---|
committer | Alex Johnston <acjohnston@google.com> | 2020-11-27 08:25:56 +0000 |
commit | eff614db0097971038e1cf0d6180b3b5d9a589cd (patch) | |
tree | 8a99c4e9df166326e4fd1959da0f6c27e935e2c6 | |
parent | 4b59cf44df7cd7a40dba7bd655643ed64a5b8a50 (diff) |
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
-rw-r--r-- | core/api/current.txt | 14 | ||||
-rw-r--r-- | core/tests/coretests/src/android/security/CredentialManagementAppTest.java | 185 | ||||
-rw-r--r-- | keystore/java/android/security/AppUriAuthenticationPolicy.aidl | 19 | ||||
-rw-r--r-- | keystore/java/android/security/AppUriAuthenticationPolicy.java | 226 | ||||
-rw-r--r-- | keystore/java/android/security/CredentialManagementApp.java | 123 | ||||
-rw-r--r-- | keystore/java/android/security/Credentials.java | 2 | ||||
-rw-r--r-- | keystore/java/android/security/KeyChain.java | 38 | ||||
-rw-r--r-- | keystore/java/android/security/UrisToAliases.java | 138 |
8 files changed, 745 insertions, 0 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index b86e16fadbb5..e302b25873b4 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -40978,6 +40978,19 @@ package android.se.omapi { package android.security { + public final class AppUriAuthenticationPolicy implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.Map<java.lang.String,java.util.Map<android.net.Uri,java.lang.String>> getAppAndUriMappings(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.security.AppUriAuthenticationPolicy> CREATOR; + } + + public static final class AppUriAuthenticationPolicy.Builder { + ctor public AppUriAuthenticationPolicy.Builder(); + method @NonNull public android.security.AppUriAuthenticationPolicy.Builder addAppAndUriMapping(@NonNull String, @NonNull android.net.Uri, @NonNull String); + method @NonNull public android.security.AppUriAuthenticationPolicy build(); + } + public final class AttestedKeyPair { ctor public AttestedKeyPair(@Nullable java.security.KeyPair, @NonNull java.util.List<java.security.cert.Certificate>); method @NonNull public java.util.List<java.security.cert.Certificate> getAttestationRecord(); @@ -41025,6 +41038,7 @@ package android.security { method public static void choosePrivateKeyAlias(@NonNull android.app.Activity, @NonNull android.security.KeyChainAliasCallback, @Nullable String[], @Nullable java.security.Principal[], @Nullable String, int, @Nullable String); method public static void choosePrivateKeyAlias(@NonNull android.app.Activity, @NonNull android.security.KeyChainAliasCallback, @Nullable String[], @Nullable java.security.Principal[], @Nullable android.net.Uri, @Nullable String); method @NonNull public static android.content.Intent createInstallIntent(); + method @NonNull public static android.content.Intent createManageCredentialsIntent(@NonNull android.security.AppUriAuthenticationPolicy); method @Nullable @WorkerThread public static java.security.cert.X509Certificate[] getCertificateChain(@NonNull android.content.Context, @NonNull String) throws java.lang.InterruptedException, android.security.KeyChainException; method @Nullable @WorkerThread public static java.security.PrivateKey getPrivateKey(@NonNull android.content.Context, @NonNull String) throws java.lang.InterruptedException, android.security.KeyChainException; method @Deprecated public static boolean isBoundKeyAlgorithm(@NonNull String); diff --git a/core/tests/coretests/src/android/security/CredentialManagementAppTest.java b/core/tests/coretests/src/android/security/CredentialManagementAppTest.java new file mode 100644 index 000000000000..366aabd183e3 --- /dev/null +++ b/core/tests/coretests/src/android/security/CredentialManagementAppTest.java @@ -0,0 +1,185 @@ +/* + * 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 static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import android.net.Uri; +import android.util.Xml; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.Map; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class CredentialManagementAppTest { + + private static final String TEST_PACKAGE_NAME_1 = "com.android.test"; + private static final String TEST_PACKAGE_NAME_2 = "com.android.test2"; + private static final Uri TEST_URI_1 = Uri.parse("test.com"); + private static final Uri TEST_URI_2 = Uri.parse("test2.com"); + private static final String TEST_ALIAS_1 = "testAlias"; + private static final String TEST_ALIAS_2 = "testAlias2"; + + private static final String PACKAGE_NAME = "com.android.cred.mng.pkg"; + private static final AppUriAuthenticationPolicy AUTHENTICATION_POLICY = + new AppUriAuthenticationPolicy.Builder() + .addAppAndUriMapping(TEST_PACKAGE_NAME_1, TEST_URI_1, TEST_ALIAS_1) + .build(); + private static final CredentialManagementApp CREDENTIAL_MANAGEMENT_APP = + new CredentialManagementApp(PACKAGE_NAME, AUTHENTICATION_POLICY); + + private static final String TAG_CREDENTIAL_MANAGEMENT_APP = "credential-management-app"; + + @Test + public void credentialManagementApp_getters() { + CredentialManagementApp credentialManagementApp = + new CredentialManagementApp(PACKAGE_NAME, AUTHENTICATION_POLICY); + + assertThat(credentialManagementApp.getPackageName(), is(PACKAGE_NAME)); + assertThat(credentialManagementApp.getAuthenticationPolicy(), is(AUTHENTICATION_POLICY)); + } + + @Test + public void setAuthenticationPolicy_updatesAuthenticationPolicy() { + CredentialManagementApp credentialManagementApp = + new CredentialManagementApp(PACKAGE_NAME, AUTHENTICATION_POLICY); + AppUriAuthenticationPolicy updatedAuthenticationPolicy = + new AppUriAuthenticationPolicy.Builder().addAppAndUriMapping( + TEST_PACKAGE_NAME_2, TEST_URI_2, TEST_ALIAS_2).build(); + + credentialManagementApp.setAuthenticationPolicy(updatedAuthenticationPolicy); + + assertThat(credentialManagementApp.getAuthenticationPolicy(), + is(updatedAuthenticationPolicy)); + } + + @Test + public void constructor_nullPackageName_throwException() { + try { + new CredentialManagementApp(/* packageName= */ null, AUTHENTICATION_POLICY); + fail("Shall not take null inputs"); + } catch (NullPointerException expected) { + // Expected behavior, nothing to do. + } + } + + @Test + public void constructor_nullAuthenticationPolicy_throwException() { + try { + new CredentialManagementApp(PACKAGE_NAME, /* authenticationPolicy= */ null); + fail("Shall not take null inputs"); + } catch (NullPointerException expected) { + // Expected behavior, nothing to do. + } + } + + @Test + public void writeToXmlAndReadFromXml() throws IOException, XmlPullParserException { + File xmlFile = writeToXml(CREDENTIAL_MANAGEMENT_APP); + + CredentialManagementApp loadedCredentialManagementApp = readFromXml(xmlFile); + + assertCredentialManagementAppsEqual(loadedCredentialManagementApp, + CREDENTIAL_MANAGEMENT_APP); + } + + private File writeToXml(CredentialManagementApp credentialManagementApp) throws IOException { + File file = File.createTempFile("temp", "credmng"); + final FileOutputStream out = new FileOutputStream(file); + XmlSerializer xml = Xml.newSerializer(); + xml.setOutput(out, StandardCharsets.UTF_8.name()); + xml.startDocument(null, true); + xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + xml.startTag(null, TAG_CREDENTIAL_MANAGEMENT_APP); + credentialManagementApp.writeToXml(xml); + xml.endTag(null, TAG_CREDENTIAL_MANAGEMENT_APP); + xml.endDocument(); + out.close(); + return file; + } + + private CredentialManagementApp readFromXml(File file) + throws IOException, XmlPullParserException { + CredentialManagementApp credentialManagementApp = null; + final XmlPullParser parser = Xml.newPullParser(); + final FileInputStream in = new FileInputStream(file); + parser.setInput(in, StandardCharsets.UTF_8.name()); + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + } + String tag = parser.getName(); + if (TAG_CREDENTIAL_MANAGEMENT_APP.equals(tag)) { + credentialManagementApp = CredentialManagementApp.readFromXml(parser); + } + return credentialManagementApp; + } + + private void assertCredentialManagementAppsEqual(CredentialManagementApp actual, + CredentialManagementApp expected) { + assertThat(actual.getPackageName(), is(expected.getPackageName())); + assertAuthenticationPoliciesEqual(actual.getAuthenticationPolicy(), + expected.getAuthenticationPolicy()); + } + + private void assertAuthenticationPoliciesEqual(AppUriAuthenticationPolicy actual, + AppUriAuthenticationPolicy expected) { + Iterator<Map.Entry<String, Map<Uri, String>>> actualIter = + actual.getAppAndUriMappings().entrySet().iterator(); + Iterator<Map.Entry<String, Map<Uri, String>>> expectedIter = + expected.getAppAndUriMappings().entrySet().iterator(); + + assertThat(actual.getAppAndUriMappings().size(), + is(expected.getAppAndUriMappings().size())); + while (actualIter.hasNext()) { + Map.Entry<String, Map<Uri, String>> actualAppToUri = actualIter.next(); + Map.Entry<String, Map<Uri, String>> expectedAppToUri = expectedIter.next(); + assertThat(actualAppToUri.getKey(), is(expectedAppToUri.getKey())); + assertUrisToAliasesEqual(actualAppToUri.getValue(), expectedAppToUri.getValue()); + } + } + + private void assertUrisToAliasesEqual(Map<Uri, String> actual, Map<Uri, String> expected) { + Iterator<Map.Entry<Uri, String>> actualIter = actual.entrySet().iterator(); + Iterator<Map.Entry<Uri, String>> expectedIter = expected.entrySet().iterator(); + + assertThat(actual.size(), is(expected.size())); + while (actualIter.hasNext()) { + Map.Entry<Uri, String> actualUriToAlias = actualIter.next(); + Map.Entry<Uri, String> expectedUriToAlias = expectedIter.next(); + assertThat(actualUriToAlias.getKey(), is(expectedUriToAlias.getKey())); + assertThat(actualUriToAlias.getValue(), is(expectedUriToAlias.getValue())); + } + } +} 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. + * <p> + * The authentication policy should be added as a parameter when calling + * {@link KeyChain#createManageCredentialsIntent}. + * <p> + * Example: + * <pre>{@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); + * }</pre> + * <p> + */ +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. + * <p> + * appPackageName -> uri -> alias + */ + @NonNull + private final Map<String, UrisToAliases> mAppToUris; + + private AppUriAuthenticationPolicy(@NonNull Map<String, UrisToAliases> appToUris) { + Objects.requireNonNull(appToUris); + this.mAppToUris = appToUris; + } + + /** + * Builder class for {@link AppUriAuthenticationPolicy} objects. + */ + public static final class Builder { + private Map<String, UrisToAliases> 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. + * <p> + * 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. + * <p> + * 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<AppUriAuthenticationPolicy> CREATOR = + new Parcelable.Creator<AppUriAuthenticationPolicy>() { + @Override + public AppUriAuthenticationPolicy createFromParcel(Parcel in) { + Map<String, UrisToAliases> 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. + * <p> + * appPackageName -> uri -> alias + */ + @NonNull + public Map<String, Map<Uri, String>> getAppAndUriMappings() { + Map<String, Map<Uri, String>> appAndUris = new HashMap<>(); + for (Map.Entry<String, UrisToAliases> 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<String, UrisToAliases> 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. + * <p> + * 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. + * <p> + * 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. + * <p> + * 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; @@ -122,6 +124,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 */ @@ -202,6 +209,20 @@ public final class KeyChain { public static final String EXTRA_PKCS12 = "PKCS12"; /** + * Extra used by {@link #createManageCredentialsIntent(AppUriAuthenticationPolicy)} to specify + * the authentication policy of the credential management app. + * + * <p>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. + * + * <p>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: * @@ -386,6 +407,23 @@ public final class KeyChain { } /** + * 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 * selected alias or null will be returned via 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<Uri, String> mUrisToAliases; + + public UrisToAliases() { + this.mUrisToAliases = new HashMap<>(); + } + + private UrisToAliases(@NonNull Map<Uri, String> urisToAliases) { + this.mUrisToAliases = urisToAliases; + } + + @NonNull + public static final Creator<UrisToAliases> CREATOR = new Creator<UrisToAliases>() { + @Override + public UrisToAliases createFromParcel(Parcel in) { + Map<Uri, String> 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<Uri, String> 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<Uri, String> 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<Uri, String> 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); + } +} |