1/*
2 * Copyright (C) 2011 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.keychain.tests;
18
19import android.app.Activity;
20import android.content.Intent;
21import android.os.AsyncTask;
22import android.os.Bundle;
23import android.os.StrictMode;
24import android.security.KeyChain;
25import android.security.KeyChainAliasCallback;
26import android.security.KeyChainException;
27import android.text.method.ScrollingMovementMethod;
28import android.util.Log;
29import android.widget.TextView;
30import java.net.Socket;
31import java.net.URL;
32import java.security.KeyStore;
33import java.security.Principal;
34import java.security.PrivateKey;
35import java.security.cert.CertificateException;
36import java.security.cert.X509Certificate;
37import javax.net.ssl.HttpsURLConnection;
38import javax.net.ssl.KeyManager;
39import javax.net.ssl.KeyManagerFactory;
40import javax.net.ssl.SSLContext;
41import javax.net.ssl.SSLSocketFactory;
42import javax.net.ssl.TrustManager;
43import javax.net.ssl.X509ExtendedKeyManager;
44import javax.net.ssl.X509TrustManager;
45import libcore.java.security.TestKeyStore;
46import libcore.javax.net.ssl.TestSSLContext;
47import com.google.mockwebserver.MockResponse;
48import com.google.mockwebserver.MockWebServer;
49
50/**
51 * Simple activity based test that exercises the KeyChain API
52 */
53public class KeyChainTestActivity extends Activity {
54
55    private static final String TAG = "KeyChainTestActivity";
56
57    private static final int REQUEST_CA_INSTALL = 1;
58
59    private TextView mTextView;
60
61    private TestKeyStore mTestKeyStore;
62
63    private final Object mAliasLock = new Object();
64    private String mAlias;
65
66    private final Object mGrantedLock = new Object();
67    private boolean mGranted;
68
69    private void log(final String message) {
70        Log.d(TAG, message);
71        runOnUiThread(new Runnable() {
72            @Override public void run() {
73                mTextView.append(message + "\n");
74            }
75        });
76    }
77
78    @Override public void onCreate(Bundle savedInstanceState) {
79        super.onCreate(savedInstanceState);
80
81        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
82                                   .detectDiskReads()
83                                   .detectDiskWrites()
84                                   .detectAll()
85                                   .penaltyLog()
86                                   .penaltyDeath()
87                                   .build());
88        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
89                               .detectLeakedSqlLiteObjects()
90                               .detectLeakedClosableObjects()
91                               .penaltyLog()
92                               .penaltyDeath()
93                               .build());
94
95        mTextView = new TextView(this);
96        mTextView.setMovementMethod(new ScrollingMovementMethod());
97        setContentView(mTextView);
98
99        log("Starting test...");
100        testKeyChainImproperUse();
101
102        new SetupTestKeyStore().execute();
103    }
104
105    private void testKeyChainImproperUse() {
106        try {
107            KeyChain.getPrivateKey(null, null);
108            throw new AssertionError();
109        } catch (InterruptedException e) {
110            throw new AssertionError(e);
111        } catch (KeyChainException e) {
112            throw new AssertionError(e);
113        } catch (NullPointerException expected) {
114            log("KeyChain failed as expected with null argument.");
115        }
116
117        try {
118            KeyChain.getPrivateKey(this, null);
119            throw new AssertionError();
120        } catch (InterruptedException e) {
121            throw new AssertionError(e);
122        } catch (KeyChainException e) {
123            throw new AssertionError(e);
124        } catch (NullPointerException expected) {
125            log("KeyChain failed as expected with null argument.");
126        }
127
128        try {
129            KeyChain.getPrivateKey(null, "");
130            throw new AssertionError();
131        } catch (InterruptedException e) {
132            throw new AssertionError(e);
133        } catch (KeyChainException e) {
134            throw new AssertionError(e);
135        } catch (NullPointerException expected) {
136            log("KeyChain failed as expected with null argument.");
137        }
138
139        try {
140            KeyChain.getPrivateKey(this, "");
141            throw new AssertionError();
142        } catch (InterruptedException e) {
143            throw new AssertionError(e);
144        } catch (KeyChainException e) {
145            throw new AssertionError(e);
146        } catch (IllegalStateException expected) {
147            log("KeyChain failed as expected on main thread.");
148        }
149    }
150
151    private class SetupTestKeyStore extends AsyncTask<Void, Void, Void> {
152        @Override protected Void doInBackground(Void... params) {
153            mTestKeyStore = TestKeyStore.getServer();
154            return null;
155        }
156        @Override protected void onPostExecute(Void result) {
157            testCaInstall();
158        }
159    }
160
161    private void testCaInstall() {
162        try {
163            log("Requesting install of server's CA...");
164            X509Certificate ca = mTestKeyStore.getRootCertificate("RSA");
165            Intent intent = KeyChain.createInstallIntent();
166            intent.putExtra(KeyChain.EXTRA_NAME, TAG);
167            intent.putExtra(KeyChain.EXTRA_CERTIFICATE, ca.getEncoded());
168            startActivityForResult(intent, REQUEST_CA_INSTALL);
169        } catch (Exception e) {
170            throw new AssertionError(e);
171        }
172
173    }
174
175    private class TestHttpsRequest extends AsyncTask<Void, Void, Void> {
176        @Override protected Void doInBackground(Void... params) {
177            try {
178                log("Starting web server...");
179                URL url = startWebServer();
180                log("Making https request to " + url);
181                makeHttpsRequest(url);
182                log("Tests succeeded.");
183
184                return null;
185            } catch (Exception e) {
186                throw new AssertionError(e);
187            }
188        }
189        private URL startWebServer() throws Exception {
190            KeyStore serverKeyStore = mTestKeyStore.keyStore;
191            char[] serverKeyStorePassword = mTestKeyStore.storePassword;
192            String kmfAlgoritm = KeyManagerFactory.getDefaultAlgorithm();
193            KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgoritm);
194            kmf.init(serverKeyStore, serverKeyStorePassword);
195            SSLContext serverContext = SSLContext.getInstance("SSL");
196            serverContext.init(kmf.getKeyManagers(),
197                               new TrustManager[] { new TrustAllTrustManager() },
198                               null);
199            SSLSocketFactory sf = serverContext.getSocketFactory();
200            SSLSocketFactory needClientAuth = TestSSLContext.clientAuth(sf, false, true);
201            MockWebServer server = new MockWebServer();
202            server.useHttps(needClientAuth, false);
203            server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
204            server.play();
205            return server.getUrl("/");
206        }
207        private void makeHttpsRequest(URL url) throws Exception {
208            SSLContext clientContext = SSLContext.getInstance("SSL");
209            clientContext.init(new KeyManager[] { new KeyChainKeyManager() }, null, null);
210            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
211            connection.setSSLSocketFactory(clientContext.getSocketFactory());
212            if (connection.getResponseCode() != 200) {
213                throw new AssertionError();
214            }
215        }
216    }
217
218    private class KeyChainKeyManager extends X509ExtendedKeyManager {
219        @Override public String chooseClientAlias(String[] keyTypes,
220                                                  Principal[] issuers,
221                                                  Socket socket) {
222            log("KeyChainKeyManager chooseClientAlias...");
223
224            KeyChain.choosePrivateKeyAlias(KeyChainTestActivity.this, new AliasResponse(),
225                                           keyTypes, issuers,
226                                           socket.getInetAddress().getHostName(), socket.getPort(),
227                                           "My Test Certificate");
228            String alias;
229            synchronized (mAliasLock) {
230                while (mAlias == null) {
231                    try {
232                        mAliasLock.wait();
233                    } catch (InterruptedException ignored) {
234                    }
235                }
236                alias = mAlias;
237            }
238            return alias;
239        }
240        @Override public String chooseServerAlias(String keyType,
241                                                  Principal[] issuers,
242                                                  Socket socket) {
243            // not a client SSLSocket callback
244            throw new UnsupportedOperationException();
245        }
246        @Override public X509Certificate[] getCertificateChain(String alias) {
247            try {
248                log("KeyChainKeyManager getCertificateChain...");
249                X509Certificate[] certificateChain
250                        = KeyChain.getCertificateChain(KeyChainTestActivity.this, alias);
251                if (certificateChain == null) {
252                    log("Null certificate chain!");
253                    return null;
254                }
255                for (int i = 0; i < certificateChain.length; i++) {
256                    log("certificate[" + i + "]=" + certificateChain[i]);
257                }
258                return certificateChain;
259            } catch (InterruptedException e) {
260                Thread.currentThread().interrupt();
261                return null;
262            } catch (KeyChainException e) {
263                throw new RuntimeException(e);
264            }
265        }
266        @Override public String[] getClientAliases(String keyType, Principal[] issuers) {
267            // not a client SSLSocket callback
268            throw new UnsupportedOperationException();
269        }
270        @Override public String[] getServerAliases(String keyType, Principal[] issuers) {
271            // not a client SSLSocket callback
272            throw new UnsupportedOperationException();
273        }
274        @Override public PrivateKey getPrivateKey(String alias) {
275            try {
276                log("KeyChainKeyManager getPrivateKey...");
277                PrivateKey privateKey = KeyChain.getPrivateKey(KeyChainTestActivity.this,
278                                                                         alias);
279                log("privateKey=" + privateKey);
280                return privateKey;
281            } catch (InterruptedException e) {
282                Thread.currentThread().interrupt();
283                return null;
284            } catch (KeyChainException e) {
285                throw new RuntimeException(e);
286            }
287        }
288    }
289
290    private class AliasResponse implements KeyChainAliasCallback {
291        @Override public void alias(String alias) {
292            if (alias == null) {
293                log("AliasResponse empty!");
294                log("Do you need to install some client certs with:");
295                log("    adb shell am startservice -n "
296                    + "com.android.keychain.tests/.KeyChainServiceTest");
297                return;
298            }
299            log("Alias choosen '" + alias + "'");
300            synchronized (mAliasLock) {
301                mAlias = alias;
302                mAliasLock.notifyAll();
303            }
304        }
305    }
306
307    private static class TrustAllTrustManager implements X509TrustManager {
308        @Override public void checkClientTrusted(X509Certificate[] chain, String authType)
309                throws CertificateException {
310        }
311        @Override public void checkServerTrusted(X509Certificate[] chain, String authType)
312                throws CertificateException {
313        }
314        @Override public X509Certificate[] getAcceptedIssuers() {
315            return null;
316        }
317    }
318
319    @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) {
320        switch (requestCode) {
321            case REQUEST_CA_INSTALL: {
322                log("onActivityResult REQUEST_CA_INSTALL...");
323                if (resultCode != RESULT_OK) {
324                    log("REQUEST_CA_INSTALL failed!");
325                    return;
326                }
327                new TestHttpsRequest().execute();
328                break;
329            }
330            default:
331                throw new IllegalStateException("requestCode == " + requestCode);
332        }
333    }
334}
335