41cd6e98c29d058f5f33d1048e9a8a688f9a0ba4
[strongswan.git] / src / frontends / android / src / org / strongswan / android / ui / VpnProfileDetailActivity.java
1 /*
2 * Copyright (C) 2012-2014 Tobias Brunner
3 * Copyright (C) 2012 Giuliano Grassi
4 * Copyright (C) 2012 Ralf Sager
5 * Hochschule fuer Technik Rapperswil
6 *
7 * This program is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation; either version 2 of the License, or (at your
10 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 * for more details.
16 */
17
18 package org.strongswan.android.ui;
19
20 import java.security.cert.X509Certificate;
21
22 import org.strongswan.android.R;
23 import org.strongswan.android.data.VpnProfile;
24 import org.strongswan.android.data.VpnProfileDataSource;
25 import org.strongswan.android.data.VpnType;
26 import org.strongswan.android.data.VpnType.VpnTypeFeature;
27 import org.strongswan.android.logic.TrustedCertificateManager;
28 import org.strongswan.android.security.TrustedCertificateEntry;
29
30 import android.app.Activity;
31 import android.app.AlertDialog;
32 import android.app.Dialog;
33 import android.app.DialogFragment;
34 import android.content.Context;
35 import android.content.DialogInterface;
36 import android.content.Intent;
37 import android.os.AsyncTask;
38 import android.os.Bundle;
39 import android.security.KeyChain;
40 import android.security.KeyChainAliasCallback;
41 import android.security.KeyChainException;
42 import android.text.Html;
43 import android.util.Log;
44 import android.view.Menu;
45 import android.view.MenuInflater;
46 import android.view.MenuItem;
47 import android.view.View;
48 import android.view.View.OnClickListener;
49 import android.view.ViewGroup;
50 import android.widget.AdapterView;
51 import android.widget.AdapterView.OnItemSelectedListener;
52 import android.widget.CheckBox;
53 import android.widget.CompoundButton;
54 import android.widget.CompoundButton.OnCheckedChangeListener;
55 import android.widget.EditText;
56 import android.widget.RelativeLayout;
57 import android.widget.Spinner;
58 import android.widget.TextView;
59
60 public class VpnProfileDetailActivity extends Activity
61 {
62 private static final int SELECT_TRUSTED_CERTIFICATE = 0;
63
64 private VpnProfileDataSource mDataSource;
65 private Long mId;
66 private TrustedCertificateEntry mCertEntry;
67 private String mUserCertLoading;
68 private TrustedCertificateEntry mUserCertEntry;
69 private VpnType mVpnType = VpnType.IKEV2_EAP;
70 private VpnProfile mProfile;
71 private EditText mName;
72 private EditText mGateway;
73 private Spinner mSelectVpnType;
74 private ViewGroup mUsernamePassword;
75 private EditText mUsername;
76 private EditText mPassword;
77 private ViewGroup mUserCertificate;
78 private RelativeLayout mSelectUserCert;
79 private CheckBox mCheckAuto;
80 private RelativeLayout mSelectCert;
81 private RelativeLayout mTncNotice;
82
83 @Override
84 public void onCreate(Bundle savedInstanceState)
85 {
86 super.onCreate(savedInstanceState);
87
88 /* the title is set when we load the profile, if any */
89 getActionBar().setDisplayHomeAsUpEnabled(true);
90
91 mDataSource = new VpnProfileDataSource(this);
92 mDataSource.open();
93
94 setContentView(R.layout.profile_detail_view);
95
96 mName = (EditText)findViewById(R.id.name);
97 mGateway = (EditText)findViewById(R.id.gateway);
98 mSelectVpnType = (Spinner)findViewById(R.id.vpn_type);
99 mTncNotice = (RelativeLayout)findViewById(R.id.tnc_notice);
100
101 mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
102 mUsername = (EditText)findViewById(R.id.username);
103 mPassword = (EditText)findViewById(R.id.password);
104
105 mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group);
106 mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate);
107
108 mCheckAuto = (CheckBox)findViewById(R.id.ca_auto);
109 mSelectCert = (RelativeLayout)findViewById(R.id.select_certificate);
110
111 mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() {
112 @Override
113 public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
114 {
115 mVpnType = VpnType.values()[position];
116 updateCredentialView();
117 }
118
119 @Override
120 public void onNothingSelected(AdapterView<?> parent)
121 { /* should not happen */
122 mVpnType = VpnType.IKEV2_EAP;
123 updateCredentialView();
124 }
125 });
126
127 ((TextView)mTncNotice.findViewById(android.R.id.text1)).setText(R.string.tnc_notice_title);
128 ((TextView)mTncNotice.findViewById(android.R.id.text2)).setText(R.string.tnc_notice_subtitle);
129 mTncNotice.setOnClickListener(new OnClickListener() {
130 @Override
131 public void onClick(View v)
132 {
133 new TncNoticeDialog().show(VpnProfileDetailActivity.this.getFragmentManager(), "TncNotice");
134 }
135 });
136
137 mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
138
139 mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() {
140 @Override
141 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
142 {
143 updateCertificateSelector();
144 }
145 });
146
147 mSelectCert.setOnClickListener(new OnClickListener() {
148 @Override
149 public void onClick(View v)
150 {
151 Intent intent = new Intent(VpnProfileDetailActivity.this, TrustedCertificatesActivity.class);
152 intent.setAction(TrustedCertificatesActivity.SELECT_CERTIFICATE);
153 startActivityForResult(intent, SELECT_TRUSTED_CERTIFICATE);
154 }
155 });
156
157 mId = savedInstanceState == null ? null : savedInstanceState.getLong(VpnProfileDataSource.KEY_ID);
158 if (mId == null)
159 {
160 Bundle extras = getIntent().getExtras();
161 mId = extras == null ? null : extras.getLong(VpnProfileDataSource.KEY_ID);
162 }
163
164 loadProfileData(savedInstanceState);
165
166 updateCredentialView();
167 updateCertificateSelector();
168 }
169
170 @Override
171 protected void onDestroy()
172 {
173 super.onDestroy();
174 mDataSource.close();
175 }
176
177 @Override
178 protected void onSaveInstanceState(Bundle outState)
179 {
180 super.onSaveInstanceState(outState);
181 if (mId != null)
182 {
183 outState.putLong(VpnProfileDataSource.KEY_ID, mId);
184 }
185 if (mUserCertEntry != null)
186 {
187 outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
188 }
189 if (mCertEntry != null)
190 {
191 outState.putString(VpnProfileDataSource.KEY_CERTIFICATE, mCertEntry.getAlias());
192 }
193 }
194
195 @Override
196 public boolean onCreateOptionsMenu(Menu menu)
197 {
198 MenuInflater inflater = getMenuInflater();
199 inflater.inflate(R.menu.profile_edit, menu);
200 return true;
201 }
202
203 @Override
204 public boolean onOptionsItemSelected(MenuItem item)
205 {
206 switch (item.getItemId())
207 {
208 case android.R.id.home:
209 case R.id.menu_cancel:
210 finish();
211 return true;
212 case R.id.menu_accept:
213 saveProfile();
214 return true;
215 default:
216 return super.onOptionsItemSelected(item);
217 }
218 }
219
220 @Override
221 protected void onActivityResult(int requestCode, int resultCode, Intent data)
222 {
223 switch (requestCode)
224 {
225 case SELECT_TRUSTED_CERTIFICATE:
226 if (resultCode == RESULT_OK)
227 {
228 String alias = data.getStringExtra(VpnProfileDataSource.KEY_CERTIFICATE);
229 X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
230 mCertEntry = certificate == null ? null : new TrustedCertificateEntry(alias, certificate);
231 updateCertificateSelector();
232 }
233 break;
234 default:
235 super.onActivityResult(requestCode, resultCode, data);
236 }
237 }
238
239 /**
240 * Update the UI to enter credentials depending on the type of VPN currently selected
241 */
242 private void updateCredentialView()
243 {
244 mUsernamePassword.setVisibility(mVpnType.has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE);
245 mUserCertificate.setVisibility(mVpnType.has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE);
246 mTncNotice.setVisibility(mVpnType.has(VpnTypeFeature.BYOD) ? View.VISIBLE : View.GONE);
247
248 if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
249 {
250 if (mUserCertLoading != null)
251 {
252 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
253 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
254 }
255 else if (mUserCertEntry != null)
256 { /* clear any errors and set the new data */
257 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
258 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
259 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
260 }
261 else
262 {
263 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label);
264 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate);
265 }
266 }
267 }
268
269 /**
270 * Show an alert in case the previously selected certificate is not found anymore
271 * or the user did not select a certificate in the spinner.
272 */
273 private void showCertificateAlert()
274 {
275 AlertDialog.Builder adb = new AlertDialog.Builder(VpnProfileDetailActivity.this);
276 adb.setTitle(R.string.alert_text_nocertfound_title);
277 adb.setMessage(R.string.alert_text_nocertfound);
278 adb.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
279 @Override
280 public void onClick(DialogInterface dialog, int id)
281 {
282 dialog.cancel();
283 }
284 });
285 adb.show();
286 }
287
288 /**
289 * Update the CA certificate selection UI depending on whether the
290 * certificate should be automatically selected or not.
291 */
292 private void updateCertificateSelector()
293 {
294 if (!mCheckAuto.isChecked())
295 {
296 mSelectCert.setEnabled(true);
297 mSelectCert.setVisibility(View.VISIBLE);
298
299 if (mCertEntry != null)
300 {
301 ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
302 ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
303 }
304 else
305 {
306 ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(R.string.profile_ca_select_certificate_label);
307 ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(R.string.profile_ca_select_certificate);
308 }
309 }
310 else
311 {
312 mSelectCert.setEnabled(false);
313 mSelectCert.setVisibility(View.GONE);
314 }
315 }
316
317 /**
318 * Save or update the profile depending on whether we actually have a
319 * profile object or not (this was created in updateProfileData)
320 */
321 private void saveProfile()
322 {
323 if (verifyInput())
324 {
325 if (mProfile != null)
326 {
327 updateProfileData();
328 mDataSource.updateVpnProfile(mProfile);
329 }
330 else
331 {
332 mProfile = new VpnProfile();
333 updateProfileData();
334 mDataSource.insertProfile(mProfile);
335 }
336 setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
337 finish();
338 }
339 }
340
341 /**
342 * Verify the user input and display error messages.
343 * @return true if the input is valid
344 */
345 private boolean verifyInput()
346 {
347 boolean valid = true;
348 if (mGateway.getText().toString().trim().isEmpty())
349 {
350 mGateway.setError(getString(R.string.alert_text_no_input_gateway));
351 valid = false;
352 }
353 if (mVpnType.has(VpnTypeFeature.USER_PASS))
354 {
355 if (mUsername.getText().toString().trim().isEmpty())
356 {
357 mUsername.setError(getString(R.string.alert_text_no_input_username));
358 valid = false;
359 }
360 }
361 if (mVpnType.has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null)
362 { /* let's show an error icon */
363 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
364 valid = false;
365 }
366 if (!mCheckAuto.isChecked() && mCertEntry == null)
367 {
368 showCertificateAlert();
369 valid = false;
370 }
371 return valid;
372 }
373
374 /**
375 * Update the profile object with the data entered by the user
376 */
377 private void updateProfileData()
378 {
379 /* the name is optional, we default to the gateway if none is given */
380 String name = mName.getText().toString().trim();
381 String gateway = mGateway.getText().toString().trim();
382 mProfile.setName(name.isEmpty() ? gateway : name);
383 mProfile.setGateway(gateway);
384 mProfile.setVpnType(mVpnType);
385 if (mVpnType.has(VpnTypeFeature.USER_PASS))
386 {
387 mProfile.setUsername(mUsername.getText().toString().trim());
388 String password = mPassword.getText().toString().trim();
389 password = password.isEmpty() ? null : password;
390 mProfile.setPassword(password);
391 }
392 if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
393 {
394 mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
395 }
396 String certAlias = mCheckAuto.isChecked() ? null : mCertEntry.getAlias();
397 mProfile.setCertificateAlias(certAlias);
398 }
399
400 /**
401 * Load an existing profile if we got an ID
402 *
403 * @param savedInstanceState previously saved state
404 */
405 private void loadProfileData(Bundle savedInstanceState)
406 {
407 String useralias = null, alias = null;
408
409 getActionBar().setTitle(R.string.add_profile);
410 if (mId != null && mId != 0)
411 {
412 mProfile = mDataSource.getVpnProfile(mId);
413 if (mProfile != null)
414 {
415 mName.setText(mProfile.getName());
416 mGateway.setText(mProfile.getGateway());
417 mVpnType = mProfile.getVpnType();
418 mUsername.setText(mProfile.getUsername());
419 mPassword.setText(mProfile.getPassword());
420 useralias = mProfile.getUserCertificateAlias();
421 alias = mProfile.getCertificateAlias();
422 getActionBar().setTitle(mProfile.getName());
423 }
424 else
425 {
426 Log.e(VpnProfileDetailActivity.class.getSimpleName(),
427 "VPN profile with id " + mId + " not found");
428 finish();
429 }
430 }
431
432 mSelectVpnType.setSelection(mVpnType.ordinal());
433
434 /* check if the user selected a user certificate previously */
435 useralias = savedInstanceState == null ? useralias: savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
436 if (useralias != null)
437 {
438 UserCertificateLoader loader = new UserCertificateLoader(this, useralias);
439 mUserCertLoading = useralias;
440 loader.execute();
441 }
442
443 /* check if the user selected a CA certificate previously */
444 alias = savedInstanceState == null ? alias : savedInstanceState.getString(VpnProfileDataSource.KEY_CERTIFICATE);
445 mCheckAuto.setChecked(alias == null);
446 if (alias != null)
447 {
448 X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
449 if (certificate != null)
450 {
451 mCertEntry = new TrustedCertificateEntry(alias, certificate);
452 }
453 else
454 { /* previously selected certificate is not here anymore */
455 showCertificateAlert();
456 mCertEntry = null;
457 }
458 }
459 }
460
461 private class SelectUserCertOnClickListener implements OnClickListener, KeyChainAliasCallback
462 {
463 @Override
464 public void onClick(View v)
465 {
466 String useralias = mUserCertEntry != null ? mUserCertEntry.getAlias() : null;
467 KeyChain.choosePrivateKeyAlias(VpnProfileDetailActivity.this, this, new String[] { "RSA" }, null, null, -1, useralias);
468 }
469
470 @Override
471 public void alias(final String alias)
472 {
473 if (alias != null)
474 { /* otherwise the dialog was canceled, the request denied */
475 try
476 {
477 final X509Certificate[] chain = KeyChain.getCertificateChain(VpnProfileDetailActivity.this, alias);
478 /* alias() is not called from our main thread */
479 runOnUiThread(new Runnable() {
480 @Override
481 public void run()
482 {
483 if (chain != null && chain.length > 0)
484 {
485 mUserCertEntry = new TrustedCertificateEntry(alias, chain[0]);
486 }
487 updateCredentialView();
488 }
489 });
490 }
491 catch (KeyChainException e)
492 {
493 e.printStackTrace();
494 }
495 catch (InterruptedException e)
496 {
497 e.printStackTrace();
498 }
499 }
500 }
501 }
502
503 /**
504 * Load the selected user certificate asynchronously. This cannot be done
505 * from the main thread as getCertificateChain() calls back to our main
506 * thread to bind to the KeyChain service resulting in a deadlock.
507 */
508 private class UserCertificateLoader extends AsyncTask<Void, Void, X509Certificate>
509 {
510 private final Context mContext;
511 private final String mAlias;
512
513 public UserCertificateLoader(Context context, String alias)
514 {
515 mContext = context;
516 mAlias = alias;
517 }
518
519 @Override
520 protected X509Certificate doInBackground(Void... params)
521 {
522 X509Certificate[] chain = null;
523 try
524 {
525 chain = KeyChain.getCertificateChain(mContext, mAlias);
526 }
527 catch (KeyChainException e)
528 {
529 e.printStackTrace();
530 }
531 catch (InterruptedException e)
532 {
533 e.printStackTrace();
534 }
535 if (chain != null && chain.length > 0)
536 {
537 return chain[0];
538 }
539 return null;
540 }
541
542 @Override
543 protected void onPostExecute(X509Certificate result)
544 {
545 if (result != null)
546 {
547 mUserCertEntry = new TrustedCertificateEntry(mAlias, result);
548 }
549 else
550 { /* previously selected certificate is not here anymore */
551 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
552 mUserCertEntry = null;
553 }
554 mUserCertLoading = null;
555 updateCredentialView();
556 }
557 }
558
559 /**
560 * Dialog with notification message if EAP-TNC is used.
561 */
562 public static class TncNoticeDialog extends DialogFragment
563 {
564 @Override
565 public Dialog onCreateDialog(Bundle savedInstanceState)
566 {
567 return new AlertDialog.Builder(getActivity())
568 .setTitle(R.string.tnc_notice_title)
569 .setMessage(Html.fromHtml(getString(R.string.tnc_notice_details)))
570 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
571 @Override
572 public void onClick(DialogInterface dialog, int id)
573 {
574 dialog.dismiss();
575 }
576 }).create();
577 }
578 }
579 }