1/*
2 * Copyright (C) 2013 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.android.settings.vpn2;
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.nio.charset.StandardCharsets;
40import java.security.KeyStoreException;
41import java.security.NoSuchAlgorithmException;
42import java.security.KeyStore.PasswordProtection;
43import java.security.KeyStore.PrivateKeyEntry;
44import java.security.PrivateKey;
45import java.security.UnrecoverableEntryException;
46import java.security.cert.Certificate;
47import java.security.cert.CertificateEncodingException;
48import java.security.cert.CertificateException;
49import java.security.cert.X509Certificate;
50import java.util.ArrayList;
51import java.util.Collections;
52import java.util.Enumeration;
53import java.util.List;
54
55/**
56 * Certificate installer helper to extract information from a provided file
57 * and install certificates to keystore.
58 */
59public class CertInstallerHelper {
60    private static final String TAG = "CertInstallerHelper";
61    /* Define a password to unlock keystore after it is reset */
62    private static final String CERT_STORE_PASSWORD = "password";
63    private final int mUid = KeyStore.UID_SELF;
64    private PrivateKey mUserKey;  // private key
65    private X509Certificate mUserCert;  // user certificate
66    private List<X509Certificate> mCaCerts = new ArrayList<X509Certificate>();
67    private KeyStore mKeyStore = KeyStore.getInstance();
68
69    /**
70     * Unlock keystore and set password
71     */
72    public CertInstallerHelper() {
73        mKeyStore.reset();
74        mKeyStore.onUserPasswordChanged(CERT_STORE_PASSWORD);
75    }
76
77    private void extractCertificate(String certFile, String password) {
78        InputStream in = null;
79        final byte[] raw;
80        java.security.KeyStore keystore = null;
81        try {
82            // Read .p12 file from SDCARD and extract with password
83            in = new FileInputStream(new File(
84                    Environment.getExternalStorageDirectory(), certFile));
85            raw = Streams.readFully(in);
86
87            keystore = java.security.KeyStore.getInstance("PKCS12");
88            PasswordProtection passwordProtection = new PasswordProtection(password.toCharArray());
89            keystore.load(new ByteArrayInputStream(raw), passwordProtection.getPassword());
90
91            // Install certificates and private keys
92            Enumeration<String> aliases = keystore.aliases();
93            if (!aliases.hasMoreElements()) {
94                Assert.fail("key store failed to put in keychain");
95            }
96            ArrayList<String> aliasesList = Collections.list(aliases);
97            // The keystore is initialized for each test case, there will
98            // be only one alias in the keystore
99            Assert.assertEquals(1, aliasesList.size());
100            String alias = aliasesList.get(0);
101            java.security.KeyStore.Entry entry = keystore.getEntry(alias, passwordProtection);
102            Log.d(TAG, "extracted alias = " + alias + ", entry=" + entry.getClass());
103
104            if (entry instanceof PrivateKeyEntry) {
105                Assert.assertTrue(installFrom((PrivateKeyEntry) entry));
106            }
107        } catch (IOException e) {
108            Assert.fail("Failed to read certficate: " + e);
109        } catch (KeyStoreException e) {
110            Log.e(TAG, "failed to extract certificate" + e);
111        } catch (NoSuchAlgorithmException e) {
112            Log.e(TAG, "failed to extract certificate" + e);
113        } catch (CertificateException e) {
114            Log.e(TAG, "failed to extract certificate" + e);
115        } catch (UnrecoverableEntryException e) {
116            Log.e(TAG, "failed to extract certificate" + e);
117        }
118        finally {
119            if (in != null) {
120                try {
121                    in.close();
122                } catch (IOException e) {
123                    Log.e(TAG, "close FileInputStream error: " + e);
124                }
125            }
126        }
127    }
128
129    /**
130     * Extract private keys, user certificates and ca certificates
131     */
132    private synchronized boolean installFrom(PrivateKeyEntry entry) {
133        mUserKey = entry.getPrivateKey();
134        mUserCert = (X509Certificate) entry.getCertificate();
135
136        Certificate[] certs = entry.getCertificateChain();
137        Log.d(TAG, "# certs extracted = " + certs.length);
138        mCaCerts = new ArrayList<X509Certificate>(certs.length);
139        for (Certificate c : certs) {
140            X509Certificate cert = (X509Certificate) c;
141            if (isCa(cert)) {
142                mCaCerts.add(cert);
143            }
144        }
145        Log.d(TAG, "# ca certs extracted = " + mCaCerts.size());
146        return true;
147    }
148
149    private boolean isCa(X509Certificate cert) {
150        try {
151            byte[] asn1EncodedBytes = cert.getExtensionValue("2.5.29.19");
152            if (asn1EncodedBytes == null) {
153                return false;
154            }
155            DEROctetString derOctetString = (DEROctetString)
156                    new ASN1InputStream(asn1EncodedBytes).readObject();
157            byte[] octets = derOctetString.getOctets();
158            ASN1Sequence sequence = (ASN1Sequence)
159                    new ASN1InputStream(octets).readObject();
160            return BasicConstraints.getInstance(sequence).isCA();
161        } catch (IOException e) {
162            return false;
163        }
164    }
165
166    /**
167     * Extract certificate from the given file, and install it to keystore
168     * @param name certificate name
169     * @param certFile .p12 file which includes certificates
170     * @param password password to extract the .p12 file
171     */
172    public void installCertificate(VpnProfile profile, String certFile, String password) {
173        // extract private keys, certificates from the provided file
174        extractCertificate(certFile, password);
175        // install certificate to the keystore
176        int flags = KeyStore.FLAG_ENCRYPTED;
177        try {
178            if (mUserKey != null) {
179                Log.v(TAG, "has private key");
180                String key = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert;
181                byte[] value = mUserKey.getEncoded();
182
183                if (!mKeyStore.importKey(key, value, mUid, flags)) {
184                    Log.e(TAG, "Failed to install " + key + " as user " + mUid);
185                    return;
186                }
187                Log.v(TAG, "install " + key + " as user " + mUid + " is successful");
188            }
189
190            if (mUserCert != null) {
191                String certName = Credentials.USER_CERTIFICATE + profile.ipsecUserCert;
192                byte[] certData = Credentials.convertToPem(mUserCert);
193
194                if (!mKeyStore.put(certName, certData, mUid, flags)) {
195                    Log.e(TAG, "Failed to install " + certName + " as user " + mUid);
196                    return;
197                }
198                Log.v(TAG, "install " + certName + " as user" + mUid + " is successful.");
199            }
200
201            if (!mCaCerts.isEmpty()) {
202                String caListName = Credentials.CA_CERTIFICATE + profile.ipsecCaCert;
203                X509Certificate[] caCerts = mCaCerts.toArray(new X509Certificate[mCaCerts.size()]);
204                byte[] caListData = Credentials.convertToPem(caCerts);
205
206                if (!mKeyStore.put(caListName, caListData, mUid, flags)) {
207                    Log.e(TAG, "Failed to install " + caListName + " as user " + mUid);
208                    return;
209                }
210                Log.v(TAG, " install " + caListName + " as user " + mUid + " is successful");
211            }
212        } catch (CertificateEncodingException e) {
213            Log.e(TAG, "Exception while convert certificates to pem " + e);
214            throw new AssertionError(e);
215        } catch (IOException e) {
216            Log.e(TAG, "IOException while convert to pem: " + e);
217        }
218    }
219
220    public int getUid() {
221        return mUid;
222    }
223}
224