1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.googlecode.android_scripting.facade;
18
19import android.os.Environment;
20import android.security.Credentials;
21import android.security.KeyStore;
22import android.util.Log;
23
24import com.android.internal.net.VpnProfile;
25import com.android.org.bouncycastle.asn1.ASN1InputStream;
26import com.android.org.bouncycastle.asn1.ASN1Sequence;
27import com.android.org.bouncycastle.asn1.DEROctetString;
28import com.android.org.bouncycastle.asn1.x509.BasicConstraints;
29
30import junit.framework.Assert;
31
32import libcore.io.Streams;
33
34import java.io.ByteArrayInputStream;
35import java.io.File;
36import java.io.FileInputStream;
37import java.io.IOException;
38import java.io.InputStream;
39import java.security.KeyStoreException;
40import java.security.NoSuchAlgorithmException;
41import java.security.KeyStore.PasswordProtection;
42import java.security.KeyStore.PrivateKeyEntry;
43import java.security.PrivateKey;
44import java.security.UnrecoverableEntryException;
45import java.security.cert.Certificate;
46import java.security.cert.CertificateEncodingException;
47import java.security.cert.CertificateException;
48import java.security.cert.X509Certificate;
49import java.util.ArrayList;
50import java.util.Collections;
51import java.util.Enumeration;
52import java.util.List;
53
54/**
55 * Certificate installer helper to extract information from a provided file
56 * and install certificates to keystore.
57 */
58public class CertInstallerHelper {
59    private static final String TAG = "CertInstallerHelper";
60    /* Define a password to unlock keystore after it is reset */
61    private static final String CERT_STORE_PASSWORD = "password";
62    private final int mUid = KeyStore.UID_SELF;
63    private PrivateKey mUserKey;  // private key
64    private X509Certificate mUserCert;  // user certificate
65    private List<X509Certificate> mCaCerts = new ArrayList<X509Certificate>();
66    private KeyStore mKeyStore = KeyStore.getInstance();
67
68    /**
69     * Unlock keystore and set password
70     */
71    public CertInstallerHelper() {
72        mKeyStore.reset();
73        mKeyStore.onUserPasswordChanged(CERT_STORE_PASSWORD);
74    }
75
76    private void extractCertificate(String certFile, String password) {
77        InputStream in = null;
78        final byte[] raw;
79        java.security.KeyStore keystore = null;
80        try {
81            // Read .p12 file from SDCARD and extract with password
82            in = new FileInputStream(new File(
83                    Environment.getExternalStorageDirectory(), certFile));
84            raw = Streams.readFully(in);
85
86            keystore = java.security.KeyStore.getInstance("PKCS12");
87            PasswordProtection passwordProtection = new PasswordProtection(password.toCharArray());
88            keystore.load(new ByteArrayInputStream(raw), passwordProtection.getPassword());
89
90            // Install certificates and private keys
91            Enumeration<String> aliases = keystore.aliases();
92            if (!aliases.hasMoreElements()) {
93                Assert.fail("key store failed to put in keychain");
94            }
95            ArrayList<String> aliasesList = Collections.list(aliases);
96            // The keystore is initialized for each test case, there will
97            // be only one alias in the keystore
98            Assert.assertEquals(1, aliasesList.size());
99            String alias = aliasesList.get(0);
100            java.security.KeyStore.Entry entry = keystore.getEntry(alias, passwordProtection);
101            Log.d(TAG, "extracted alias = " + alias + ", entry=" + entry.getClass());
102
103            if (entry instanceof PrivateKeyEntry) {
104                Assert.assertTrue(installFrom((PrivateKeyEntry) entry));
105            }
106        } catch (IOException e) {
107            Assert.fail("Failed to read certficate: " + e);
108        } catch (KeyStoreException e) {
109            Log.e(TAG, "failed to extract certificate" + e);
110        } catch (NoSuchAlgorithmException e) {
111            Log.e(TAG, "failed to extract certificate" + e);
112        } catch (CertificateException e) {
113            Log.e(TAG, "failed to extract certificate" + e);
114        } catch (UnrecoverableEntryException e) {
115            Log.e(TAG, "failed to extract certificate" + e);
116        }
117        finally {
118            if (in != null) {
119                try {
120                    in.close();
121                } catch (IOException e) {
122                    Log.e(TAG, "close FileInputStream error: " + e);
123                }
124            }
125        }
126    }
127
128    /**
129     * Extract private keys, user certificates and ca certificates
130     */
131    private synchronized boolean installFrom(PrivateKeyEntry entry) {
132        mUserKey = entry.getPrivateKey();
133        mUserCert = (X509Certificate) entry.getCertificate();
134
135        Certificate[] certs = entry.getCertificateChain();
136        Log.d(TAG, "# certs extracted = " + certs.length);
137        mCaCerts = new ArrayList<X509Certificate>(certs.length);
138        for (Certificate c : certs) {
139            X509Certificate cert = (X509Certificate) c;
140            if (isCa(cert)) {
141                mCaCerts.add(cert);
142            }
143        }
144        Log.d(TAG, "# ca certs extracted = " + mCaCerts.size());
145        return true;
146    }
147
148    private boolean isCa(X509Certificate cert) {
149        try {
150            byte[] asn1EncodedBytes = cert.getExtensionValue("2.5.29.19");
151            if (asn1EncodedBytes == null) {
152                return false;
153            }
154            DEROctetString derOctetString = (DEROctetString)
155                    new ASN1InputStream(asn1EncodedBytes).readObject();
156            byte[] octets = derOctetString.getOctets();
157            ASN1Sequence sequence = (ASN1Sequence)
158                    new ASN1InputStream(octets).readObject();
159            return BasicConstraints.getInstance(sequence).isCA();
160        } catch (IOException e) {
161            return false;
162        }
163    }
164
165    /**
166     * Extract certificate from the given file, and install it to keystore
167     * @param name certificate name
168     * @param certFile .p12 file which includes certificates
169     * @param password password to extract the .p12 file
170     */
171    public void installCertificate(VpnProfile profile, String certFile, String password) {
172        // extract private keys, certificates from the provided file
173        extractCertificate(certFile, password);
174        // install certificate to the keystore
175        int flags = KeyStore.FLAG_ENCRYPTED;
176        try {
177            if (mUserKey != null) {
178                Log.v(TAG, "has private key");
179                String key = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert;
180                byte[] value = mUserKey.getEncoded();
181
182                if (!mKeyStore.importKey(key, value, mUid, flags)) {
183                    Log.e(TAG, "Failed to install " + key + " as user " + mUid);
184                    return;
185                }
186                Log.v(TAG, "install " + key + " as user " + mUid + " is successful");
187            }
188
189            if (mUserCert != null) {
190                String certName = Credentials.USER_CERTIFICATE + profile.ipsecUserCert;
191                byte[] certData = Credentials.convertToPem(mUserCert);
192
193                if (!mKeyStore.put(certName, certData, mUid, flags)) {
194                    Log.e(TAG, "Failed to install " + certName + " as user " + mUid);
195                    return;
196                }
197                Log.v(TAG, "install " + certName + " as user" + mUid + " is successful.");
198            }
199
200            if (!mCaCerts.isEmpty()) {
201                String caListName = Credentials.CA_CERTIFICATE + profile.ipsecCaCert;
202                X509Certificate[] caCerts = mCaCerts.toArray(new X509Certificate[mCaCerts.size()]);
203                byte[] caListData = Credentials.convertToPem(caCerts);
204
205                if (!mKeyStore.put(caListName, caListData, mUid, flags)) {
206                    Log.e(TAG, "Failed to install " + caListName + " as user " + mUid);
207                    return;
208                }
209                Log.v(TAG, " install " + caListName + " as user " + mUid + " is successful");
210            }
211        } catch (CertificateEncodingException e) {
212            Log.e(TAG, "Exception while convert certificates to pem " + e);
213            throw new AssertionError(e);
214        } catch (IOException e) {
215            Log.e(TAG, "IOException while convert to pem: " + e);
216        }
217    }
218
219    public int getUid() {
220        return mUid;
221    }
222}
223