1/*
2 * Copyright (C) 2015 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 android.security.net.config;
18
19import android.os.Environment;
20import android.os.UserHandle;
21import android.util.ArraySet;
22import android.util.Pair;
23import java.io.BufferedInputStream;
24import java.io.File;
25import java.io.FileInputStream;
26import java.io.InputStream;
27import java.io.IOException;
28import java.security.cert.Certificate;
29import java.security.cert.CertificateException;
30import java.security.cert.CertificateFactory;
31import java.security.cert.X509Certificate;
32import java.util.Collections;
33import java.util.Set;
34import libcore.io.IoUtils;
35
36import com.android.org.conscrypt.Hex;
37import com.android.org.conscrypt.NativeCrypto;
38
39import javax.security.auth.x500.X500Principal;
40
41/**
42 * {@link CertificateSource} based on a directory where certificates are stored as individual files
43 * named after a hash of their SubjectName for more efficient lookups.
44 * @hide
45 */
46abstract class DirectoryCertificateSource implements CertificateSource {
47    private final File mDir;
48    private final Object mLock = new Object();
49    private final CertificateFactory mCertFactory;
50
51    private Set<X509Certificate> mCertificates;
52
53    protected DirectoryCertificateSource(File caDir) {
54        mDir = caDir;
55        try {
56            mCertFactory = CertificateFactory.getInstance("X.509");
57        } catch (CertificateException e) {
58            throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
59        }
60    }
61
62    protected abstract boolean isCertMarkedAsRemoved(String caFile);
63
64    @Override
65    public Set<X509Certificate> getCertificates() {
66        // TODO: loading all of these is wasteful, we should instead use a keystore style API.
67        synchronized (mLock) {
68            if (mCertificates != null) {
69                return mCertificates;
70            }
71
72            Set<X509Certificate> certs = new ArraySet<X509Certificate>();
73            if (mDir.isDirectory()) {
74                for (String caFile : mDir.list()) {
75                    if (isCertMarkedAsRemoved(caFile)) {
76                        continue;
77                    }
78                    X509Certificate cert = readCertificate(caFile);
79                    if (cert != null) {
80                        certs.add(cert);
81                    }
82                }
83            }
84            mCertificates = certs;
85            return mCertificates;
86        }
87    }
88
89    @Override
90    public X509Certificate findBySubjectAndPublicKey(final X509Certificate cert) {
91        return findCert(cert.getSubjectX500Principal(), new CertSelector() {
92            @Override
93            public boolean match(X509Certificate ca) {
94                return ca.getPublicKey().equals(cert.getPublicKey());
95            }
96        });
97    }
98
99    @Override
100    public X509Certificate findByIssuerAndSignature(final X509Certificate cert) {
101        return findCert(cert.getIssuerX500Principal(), new CertSelector() {
102            @Override
103            public boolean match(X509Certificate ca) {
104                try {
105                    cert.verify(ca.getPublicKey());
106                    return true;
107                } catch (Exception e) {
108                    return false;
109                }
110            }
111        });
112    }
113
114    @Override
115    public Set<X509Certificate> findAllByIssuerAndSignature(final X509Certificate cert) {
116        return findCerts(cert.getIssuerX500Principal(), new CertSelector() {
117            @Override
118            public boolean match(X509Certificate ca) {
119                try {
120                    cert.verify(ca.getPublicKey());
121                    return true;
122                } catch (Exception e) {
123                    return false;
124                }
125            }
126        });
127    }
128
129    @Override
130    public void handleTrustStorageUpdate() {
131        synchronized (mLock) {
132            mCertificates = null;
133        }
134    }
135
136    private static interface CertSelector {
137        boolean match(X509Certificate cert);
138    }
139
140    private Set<X509Certificate> findCerts(X500Principal subj, CertSelector selector) {
141        String hash = getHash(subj);
142        Set<X509Certificate> certs = null;
143        for (int index = 0; index >= 0; index++) {
144            String fileName = hash + "." + index;
145            if (!new File(mDir, fileName).exists()) {
146                break;
147            }
148            if (isCertMarkedAsRemoved(fileName)) {
149                continue;
150            }
151            X509Certificate cert = readCertificate(fileName);
152            if (!subj.equals(cert.getSubjectX500Principal())) {
153                continue;
154            }
155            if (selector.match(cert)) {
156                if (certs == null) {
157                    certs = new ArraySet<X509Certificate>();
158                }
159                certs.add(cert);
160            }
161        }
162        return certs != null ? certs : Collections.<X509Certificate>emptySet();
163    }
164
165    private X509Certificate findCert(X500Principal subj, CertSelector selector) {
166        String hash = getHash(subj);
167        for (int index = 0; index >= 0; index++) {
168            String fileName = hash + "." + index;
169            if (!new File(mDir, fileName).exists()) {
170                break;
171            }
172            if (isCertMarkedAsRemoved(fileName)) {
173                continue;
174            }
175            X509Certificate cert = readCertificate(fileName);
176            if (!subj.equals(cert.getSubjectX500Principal())) {
177                continue;
178            }
179            if (selector.match(cert)) {
180                return cert;
181            }
182        }
183        return null;
184    }
185
186    private String getHash(X500Principal name) {
187        int hash = NativeCrypto.X509_NAME_hash_old(name);
188        return Hex.intToHexString(hash, 8);
189    }
190
191    private X509Certificate readCertificate(String file) {
192        InputStream is = null;
193        try {
194            is = new BufferedInputStream(new FileInputStream(new File(mDir, file)));
195            return (X509Certificate) mCertFactory.generateCertificate(is);
196        } catch (CertificateException | IOException e) {
197            return null;
198        } finally {
199            IoUtils.closeQuietly(is);
200        }
201    }
202}
203