blob: a1e9314591bc10d736d2f3d67f4bd903d39e8d91 [file] [log] [blame]
/*
* 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.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.org.bouncycastle.asn1.ASN1InputStream;
import com.android.org.bouncycastle.asn1.ASN1Sequence;
import com.android.org.bouncycastle.asn1.DEROctetString;
import com.android.org.bouncycastle.asn1.x509.BasicConstraints;
import com.android.org.conscrypt.TrustedCertificateStore;
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;
/**
* 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<String, byte[]> mBundle = new HashMap<String, byte[]>();
private String mName = "";
private String mCertUsageSelected = "";
private String mReferrer = "";
private int mUid = -1;
private PrivateKey mUserKey;
private X509Certificate mUserCert;
private List<X509Certificate> mCaCerts = new ArrayList<X509Certificate>();
CredentialHelper() {
}
CredentialHelper(Intent intent) {
Bundle bundle = intent.getExtras();
if (bundle == null) {
return;
}
String name = bundle.getString(KeyChain.EXTRA_NAME);
bundle.remove(KeyChain.EXTRA_NAME);
if (name != null) {
mName = name;
}
String certUsageSelected = bundle.getString(Credentials.EXTRA_CERTIFICATE_USAGE);
bundle.remove(Credentials.EXTRA_CERTIFICATE_USAGE);
if (certUsageSelected != null) {
setCertUsageSelectedAndUid(certUsageSelected);
} else {
mUid = bundle.getInt(Credentials.EXTRA_INSTALL_AS_UID, -1);
}
bundle.remove(Credentials.EXTRA_INSTALL_AS_UID);
String referrer = bundle.getString(Intent.EXTRA_REFERRER);
bundle.remove(Intent.EXTRA_REFERRER);
if (referrer != null) {
mReferrer = referrer;
}
Log.d(TAG, "# extras: " + bundle.size());
for (String key : bundle.keySet()) {
byte[] bytes = bundle.getByteArray(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<byte[]> certs = new ArrayList<byte[]>(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, -1);
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<byte[]> 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 = "<br>";
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<String> 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<X509Certificate>(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;
}
}