From 0aa18af47df6dcee7d1c24a8c07f48b5795d4910 Mon Sep 17 00:00:00 2001
From: Rubin Xu <rubinxu@google.com>
Date: Sun, 10 Jan 2021 15:30:37 +0000
Subject: [PATCH] Add KeyChainService credential management app APIs

(Trimmed down version)

Bug: 179180345
Test: Manual Testing
      atest com.android.keychain.KeyChainServiceRoboTest

Change-Id: I00b7df27a92f6ee4f74546f892c83290fead1112
Merged-In: I00b7df27a92f6ee4f74546f892c83290fead1112
---
 core/api/current.txt                          |  13 +
 .../security/AppUriAuthenticationPolicy.aidl  |  19 ++
 .../security/AppUriAuthenticationPolicy.java  | 241 ++++++++++++++++++
 .../security/CredentialManagementApp.java     | 123 +++++++++
 .../android/security/IKeyChainService.aidl    |   9 +
 .../java/android/security/UrisToAliases.java  | 138 ++++++++++
 6 files changed, 543 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

diff --git a/core/api/current.txt b/core/api/current.txt
index 285506f0556d..59d595c12686 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -35918,6 +35918,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();
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..0244ce97c0d4
--- /dev/null
+++ b/keystore/java/android/security/AppUriAuthenticationPolicy.java
@@ -0,0 +1,241 @@
+/*
+ * 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.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * 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);
+        }
+    }
+
+    /**
+     * Get the set of aliases found in the policy.
+     *
+     * @hide
+     */
+    public Set<String> getAliases() {
+        Set<String> aliases = new HashSet<>();
+        for (UrisToAliases appsToUris : mAppToUris.values()) {
+            aliases.addAll(appsToUris.getUrisToAliases().values());
+        }
+        return aliases;
+    }
+
+}
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/IKeyChainService.aidl b/keystore/java/android/security/IKeyChainService.aidl
index 1ae6a631dbcb..add52fa5b436 100644
--- a/keystore/java/android/security/IKeyChainService.aidl
+++ b/keystore/java/android/security/IKeyChainService.aidl
@@ -18,6 +18,8 @@ package android.security;
 import android.content.pm.StringParceledListSlice;
 import android.security.keymaster.KeymasterCertificateChain;
 import android.security.keystore.ParcelableKeyGenParameterSpec;
+import android.security.AppUriAuthenticationPolicy;
+import android.net.Uri;
 
 /**
  * Caller is required to ensure that {@link KeyStore#unlock
@@ -56,6 +58,13 @@ interface IKeyChainService {
     boolean containsCaAlias(String alias);
     byte[] getEncodedCaCertificate(String alias, boolean includeDeletedSystem);
     List<String> getCaCertificateChainAliases(String rootAlias, boolean includeDeletedSystem);
+    void setCredentialManagementApp(String packageName, in AppUriAuthenticationPolicy policy);
+    void updateCredentialManagementAppPolicy(in AppUriAuthenticationPolicy policy);
+    boolean hasCredentialManagementApp();
+    String getCredentialManagementAppPackageName();
+    AppUriAuthenticationPolicy getCredentialManagementAppPolicy();
+    String getPredefinedAliasForPackageAndUri(String packageName, in Uri uri);
+    void removeCredentialManagementApp();
 
     // APIs used by KeyChainActivity
     void setGrant(int uid, String alias, boolean value);
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);
+    }
+}
-- 
GitLab