Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit eff614db authored by Alex Johnston's avatar Alex Johnston
Browse files

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
parent 4b59cf44
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -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);
+185 −0
Original line number Diff line number Diff line
/*
 * 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()));
        }
    }
}
+19 −0
Original line number Diff line number Diff line
/*
 * 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;
+226 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }

}
+123 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
Loading