/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.certinstaller; import static android.security.KeyStore.UID_SELF; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.KeyguardManager; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.security.Credentials; import android.security.IKeyChainService; import android.security.KeyChain; import android.text.Html; import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.org.conscrypt.TrustedCertificateStore; import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Sequence; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.x509.BasicConstraints; import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.KeyFactory; import java.security.KeyStore; import java.security.KeyStore.PasswordProtection; import java.security.KeyStore.PrivateKeyEntry; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.List; import java.util.Map; /** * A helper class for accessing the raw data in the intent extra and handling * certificates. */ class CredentialHelper { private static final String DATA_KEY = "data"; private static final String CERTS_KEY = "crts"; private static final String USER_KEY_ALGORITHM = "user_key_algorithm"; private static final String SETTINGS_PACKAGE = "com.android.settings"; private static final String TAG = "CredentialHelper"; // keep raw data from intent's extra private HashMap mBundle = new HashMap(); private String mName = ""; private String mCertUsageSelected = ""; private String mReferrer = ""; private int mUid = Process.INVALID_UID; private PrivateKey mUserKey; private X509Certificate mUserCert; private List mCaCerts = new ArrayList(); CredentialHelper() { } /** * @param byteMap keeps raw data from intent's extra * @param name * @param referrer * @param certUsageSelected used to assign mUid according to certificate usage * @param uid is ignored unless certUsageSelected is null */ CredentialHelper(@NonNull Map byteMap, @Nullable String name, @Nullable String referrer, @Nullable String certUsageSelected, int uid) { if (name != null) { mName = name; } if (referrer != null) { mReferrer = referrer; } if (certUsageSelected != null) { setCertUsageSelectedAndUid(certUsageSelected); } else { mUid = uid; } for (String key : byteMap.keySet()) { byte[] bytes = byteMap.get(key); Log.d(TAG, " " + key + ": " + ((bytes == null) ? -1 : bytes.length)); mBundle.put(key, bytes); } parseCert(getData(KeyChain.EXTRA_CERTIFICATE)); } synchronized void onSaveStates(Bundle outStates) { try { outStates.putSerializable(DATA_KEY, mBundle); outStates.putString(KeyChain.EXTRA_NAME, mName); outStates.putInt(Credentials.EXTRA_INSTALL_AS_UID, mUid); if (mUserKey != null) { Log.d(TAG, "Key algorithm: " + mUserKey.getAlgorithm()); outStates.putString(USER_KEY_ALGORITHM, mUserKey.getAlgorithm()); outStates.putByteArray(Credentials.USER_PRIVATE_KEY, mUserKey.getEncoded()); } ArrayList certs = new ArrayList(mCaCerts.size() + 1); if (mUserCert != null) { certs.add(mUserCert.getEncoded()); } for (X509Certificate cert : mCaCerts) { certs.add(cert.getEncoded()); } outStates.putByteArray(CERTS_KEY, Util.toBytes(certs)); } catch (CertificateEncodingException e) { throw new AssertionError(e); } } void onRestoreStates(Bundle savedStates) { mBundle = (HashMap) savedStates.getSerializable(DATA_KEY); mName = savedStates.getString(KeyChain.EXTRA_NAME); mUid = savedStates.getInt(Credentials.EXTRA_INSTALL_AS_UID, Process.INVALID_UID); String userKeyAlgorithm = savedStates.getString(USER_KEY_ALGORITHM); byte[] userKeyBytes = savedStates.getByteArray(Credentials.USER_PRIVATE_KEY); Log.d(TAG, "Loaded key algorithm: " + userKeyAlgorithm); if (userKeyAlgorithm != null && userKeyBytes != null) { setPrivateKey(userKeyAlgorithm, userKeyBytes); } ArrayList certs = Util.fromBytes(savedStates.getByteArray(CERTS_KEY)); for (byte[] cert : certs) { parseCert(cert); } } X509Certificate getUserCertificate() { return mUserCert; } private void parseCert(byte[] bytes) { if (bytes == null) { return; } try { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) certFactory.generateCertificate( new ByteArrayInputStream(bytes)); if (isCa(cert)) { Log.d(TAG, "got a CA cert"); mCaCerts.add(cert); } else { Log.d(TAG, "got a user cert"); mUserCert = cert; } } catch (CertificateException e) { Log.w(TAG, "parseCert(): " + e); } } private boolean isCa(X509Certificate cert) { try { // TODO: add a test about this byte[] asn1EncodedBytes = cert.getExtensionValue("2.5.29.19"); if (asn1EncodedBytes == null) { return false; } DEROctetString derOctetString = (DEROctetString) new ASN1InputStream(asn1EncodedBytes).readObject(); byte[] octets = derOctetString.getOctets(); ASN1Sequence sequence = (ASN1Sequence) new ASN1InputStream(octets).readObject(); return BasicConstraints.getInstance(sequence).isCA(); } catch (IOException e) { return false; } } boolean hasPkcs12KeyStore() { return mBundle.containsKey(KeyChain.EXTRA_PKCS12); } boolean hasPrivateKey() { return mBundle.containsKey(Credentials.EXTRA_PRIVATE_KEY); } int getUidFromCertificateUsage(String certUsage) { if (Credentials.CERTIFICATE_USAGE_WIFI.equals(certUsage)) { return Process.WIFI_UID; } else { return UID_SELF; } } boolean hasUserCertificate() { return (mUserCert != null); } boolean hasCaCerts() { return !mCaCerts.isEmpty(); } boolean hasAnyForSystemInstall() { return (mUserKey != null) || hasUserCertificate() || hasCaCerts(); } void setPrivateKey(String algorithm, byte[] bytes) { try { KeyFactory keyFactory = KeyFactory.getInstance(algorithm); mUserKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(bytes)); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } catch (InvalidKeySpecException e) { throw new AssertionError(e); } } boolean containsAnyRawData() { return !mBundle.isEmpty(); } byte[] getData(String key) { return mBundle.get(key); } void putPkcs12Data(byte[] data) { mBundle.put(KeyChain.EXTRA_PKCS12, data); } CharSequence getDescription(Context context) { // TODO: create more descriptive string StringBuilder sb = new StringBuilder(); String newline = "
"; if (mUserKey != null) { sb.append(context.getString(R.string.one_userkey)).append(newline); sb.append(context.getString(R.string.userkey_type)).append(mUserKey.getAlgorithm()) .append(newline); } if (mUserCert != null) { sb.append(context.getString(R.string.one_usercrt)).append(newline); } int n = mCaCerts.size(); if (n > 0) { if (n == 1) { sb.append(context.getString(R.string.one_cacrt)); } else { sb.append(context.getString(R.string.n_cacrts, n)); } } return Html.fromHtml(sb.toString()); } void setName(String name) { mName = name; } String getName() { return mName; } void setCertUsageSelectedAndUid(String certUsageSelected) { mCertUsageSelected = certUsageSelected; mUid = getUidFromCertificateUsage(certUsageSelected); } String getCertUsageSelected() { return mCertUsageSelected; } boolean calledBySettings() { return mReferrer != null && mReferrer.equals(SETTINGS_PACKAGE); } Intent createSystemInstallIntent(final Context context) { Intent intent = new Intent("com.android.credentials.INSTALL"); // To prevent the private key from being sniffed, we explicitly spell // out the intent receiver class. intent.setComponent(ComponentName.unflattenFromString( context.getString(R.string.config_system_install_component))); intent.putExtra(Credentials.EXTRA_INSTALL_AS_UID, mUid); intent.putExtra(Credentials.EXTRA_USER_KEY_ALIAS, mName); try { if (mUserKey != null) { intent.putExtra(Credentials.EXTRA_USER_PRIVATE_KEY_DATA, mUserKey.getEncoded()); } if (mUserCert != null) { intent.putExtra(Credentials.EXTRA_USER_CERTIFICATE_DATA, Credentials.convertToPem(mUserCert)); } if (!mCaCerts.isEmpty()) { X509Certificate[] caCerts = mCaCerts.toArray(new X509Certificate[mCaCerts.size()]); intent.putExtra(Credentials.EXTRA_CA_CERTIFICATES_DATA, Credentials.convertToPem(caCerts)); } return intent; } catch (IOException e) { throw new AssertionError(e); } catch (CertificateEncodingException e) { throw new AssertionError(e); } } boolean installVpnAndAppsTrustAnchors(Context context, IKeyChainService keyChainService) { final TrustedCertificateStore trustedCertificateStore = new TrustedCertificateStore(); for (X509Certificate caCert : mCaCerts) { byte[] bytes = null; try { bytes = caCert.getEncoded(); } catch (CertificateEncodingException e) { throw new AssertionError(e); } if (bytes != null) { try { keyChainService.installCaCertificate(bytes); } catch (RemoteException e) { Log.w(TAG, "installCaCertsToKeyChain(): " + e); return false; } String alias = trustedCertificateStore.getCertificateAlias(caCert); if (alias == null) { Log.e(TAG, "alias is null"); return false; } maybeApproveCaCert(context, alias); } } return true; } private void maybeApproveCaCert(Context context, String alias) { final KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class); if (keyguardManager.isDeviceSecure(UserHandle.myUserId())) { // Since the cert is installed by real user, the cert is approved by the user final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); dpm.approveCaCert(alias, UserHandle.myUserId(), true); } } boolean hasPassword() { if (!hasPkcs12KeyStore()) { return false; } try { return loadPkcs12Internal(new PasswordProtection(new char[] {})) == null; } catch (Exception e) { return true; } } boolean extractPkcs12(String password) { try { return extractPkcs12Internal(new PasswordProtection(password.toCharArray())); } catch (Exception e) { Log.w(TAG, "extractPkcs12(): " + e, e); return false; } } private boolean extractPkcs12Internal(PasswordProtection password) throws Exception { // TODO: add test about this java.security.KeyStore keystore = loadPkcs12Internal(password); Enumeration aliases = keystore.aliases(); if (!aliases.hasMoreElements()) { Log.e(TAG, "PKCS12 file has no elements"); return false; } while (aliases.hasMoreElements()) { String alias = aliases.nextElement(); if (keystore.isKeyEntry(alias)) { KeyStore.Entry entry = keystore.getEntry(alias, password); Log.d(TAG, "extracted alias = " + alias + ", entry=" + entry.getClass()); if (entry instanceof PrivateKeyEntry) { if (TextUtils.isEmpty(mName)) { mName = alias; } return installFrom((PrivateKeyEntry) entry); } } else { // KeyStore.getEntry with non-null ProtectionParameter can only be invoked on // PrivateKeyEntry or SecretKeyEntry. // See https://docs.oracle.com/javase/8/docs/api/java/security/KeyStore.html Log.d(TAG, "Skip non-key entry, alias = " + alias); } } return true; } private java.security.KeyStore loadPkcs12Internal(PasswordProtection password) throws Exception { java.security.KeyStore keystore = java.security.KeyStore.getInstance("PKCS12"); keystore.load(new ByteArrayInputStream(getData(KeyChain.EXTRA_PKCS12)), password.getPassword()); return keystore; } private synchronized boolean installFrom(PrivateKeyEntry entry) { mUserKey = entry.getPrivateKey(); mUserCert = (X509Certificate) entry.getCertificate(); Certificate[] certs = entry.getCertificateChain(); Log.d(TAG, "# certs extracted = " + certs.length); mCaCerts = new ArrayList(certs.length); for (Certificate c : certs) { X509Certificate cert = (X509Certificate) c; if (isCa(cert)) { mCaCerts.add(cert); } } Log.d(TAG, "# ca certs extracted = " + mCaCerts.size()); return true; } /** * Returns true if this credential contains _only_ CA certificates to be used as trust anchors * for VPN and apps. */ public boolean hasOnlyVpnAndAppsTrustAnchors() { if (!hasCaCerts()) { return false; } if (mUid != UID_SELF) { // VPN and Apps trust anchors can only be installed under UID_SELF return false; } if (mUserKey != null) { // We are installing a key pair for client authentication, its CA // should have nothing to do with VPN and apps trust anchors. return false; } else { return true; } } public String getReferrer() { return mReferrer; } @VisibleForTesting public int getUid() { return mUid; } }