8d8e07f9daaa33558628f3e7e9f5eb4cf1d433ea
[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 private static final int MTU_MIN = 1280;
64 private static final int MTU_MAX = 1500;
65
66 private VpnProfileDataSource mDataSource;
67 private Long mId;
68 private TrustedCertificateEntry mCertEntry;
69 private String mUserCertLoading;
70 private TrustedCertificateEntry mUserCertEntry;
71 private VpnType mVpnType = VpnType.IKEV2_EAP;
72 private VpnProfile mProfile;
73 private EditText mName;
74 private EditText mGateway;
75 private Spinner mSelectVpnType;
76 private ViewGroup mUsernamePassword;
77 private EditText mUsername;
78 private EditText mPassword;
79 private ViewGroup mUserCertificate;
80 private RelativeLayout mSelectUserCert;
81 private CheckBox mCheckAuto;
82 private RelativeLayout mSelectCert;
83 private RelativeLayout mTncNotice;
84 private CheckBox mShowAdvanced;
85 private ViewGroup mAdvancedSettings;
86 private EditText mMTU;
87
88 @Override
89 public void onCreate(Bundle savedInstanceState)
90 {
91 super.onCreate(savedInstanceState);
92
93 /* the title is set when we load the profile, if any */
94 getActionBar().setDisplayHomeAsUpEnabled(true);
95
96 mDataSource = new VpnProfileDataSource(this);
97 mDataSource.open();
98
99 setContentView(R.layout.profile_detail_view);
100
101 mName = (EditText)findViewById(R.id.name);
102 mGateway = (EditText)findViewById(R.id.gateway);
103 mSelectVpnType = (Spinner)findViewById(R.id.vpn_type);
104 mTncNotice = (RelativeLayout)findViewById(R.id.tnc_notice);
105
106 mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
107 mUsername = (EditText)findViewById(R.id.username);
108 mPassword = (EditText)findViewById(R.id.password);
109
110 mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group);
111 mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate);
112
113 mCheckAuto = (CheckBox)findViewById(R.id.ca_auto);
114 mSelectCert = (RelativeLayout)findViewById(R.id.select_certificate);
115
116 mShowAdvanced = (CheckBox)findViewById(R.id.show_advanced);
117 mAdvancedSettings = (ViewGroup)findViewById(R.id.advanced_settings);
118
119 mMTU = (EditText)findViewById(R.id.mtu);
120
121 mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() {
122 @Override
123 public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
124 {
125 mVpnType = VpnType.values()[position];
126 updateCredentialView();
127 }
128
129 @Override
130 public void onNothingSelected(AdapterView<?> parent)
131 { /* should not happen */
132 mVpnType = VpnType.IKEV2_EAP;
133 updateCredentialView();
134 }
135 });
136
137 ((TextView)mTncNotice.findViewById(android.R.id.text1)).setText(R.string.tnc_notice_title);
138 ((TextView)mTncNotice.findViewById(android.R.id.text2)).setText(R.string.tnc_notice_subtitle);
139 mTncNotice.setOnClickListener(new OnClickListener() {
140 @Override
141 public void onClick(View v)
142 {
143 new TncNoticeDialog().show(VpnProfileDetailActivity.this.getFragmentManager(), "TncNotice");
144 }
145 });
146
147 mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
148
149 mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() {
150 @Override
151 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
152 {
153 updateCertificateSelector();
154 }
155 });
156
157 mSelectCert.setOnClickListener(new OnClickListener() {
158 @Override
159 public void onClick(View v)
160 {
161 Intent intent = new Intent(VpnProfileDetailActivity.this, TrustedCertificatesActivity.class);
162 intent.setAction(TrustedCertificatesActivity.SELECT_CERTIFICATE);
163 startActivityForResult(intent, SELECT_TRUSTED_CERTIFICATE);
164 }
165 });
166
167 mShowAdvanced.setOnCheckedChangeListener(new OnCheckedChangeListener() {
168 @Override
169 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
170 {
171 updateAdvancedSettings();
172 }
173 });
174
175 mId = savedInstanceState == null ? null : savedInstanceState.getLong(VpnProfileDataSource.KEY_ID);
176 if (mId == null)
177 {
178 Bundle extras = getIntent().getExtras();
179 mId = extras == null ? null : extras.getLong(VpnProfileDataSource.KEY_ID);
180 }
181
182 loadProfileData(savedInstanceState);
183
184 updateCredentialView();
185 updateCertificateSelector();
186 updateAdvancedSettings();
187 }
188
189 @Override
190 protected void onDestroy()
191 {
192 super.onDestroy();
193 mDataSource.close();
194 }
195
196 @Override
197 protected void onSaveInstanceState(Bundle outState)
198 {
199 super.onSaveInstanceState(outState);
200 if (mId != null)
201 {
202 outState.putLong(VpnProfileDataSource.KEY_ID, mId);
203 }
204 if (mUserCertEntry != null)
205 {
206 outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
207 }
208 if (mCertEntry != null)
209 {
210 outState.putString(VpnProfileDataSource.KEY_CERTIFICATE, mCertEntry.getAlias());
211 }
212 }
213
214 @Override
215 public boolean onCreateOptionsMenu(Menu menu)
216 {
217 MenuInflater inflater = getMenuInflater();
218 inflater.inflate(R.menu.profile_edit, menu);
219 return true;
220 }
221
222 @Override
223 public boolean onOptionsItemSelected(MenuItem item)
224 {
225 switch (item.getItemId())
226 {
227 case android.R.id.home:
228 case R.id.menu_cancel:
229 finish();
230 return true;
231 case R.id.menu_accept:
232 saveProfile();
233 return true;
234 default:
235 return super.onOptionsItemSelected(item);
236 }
237 }
238
239 @Override
240 protected void onActivityResult(int requestCode, int resultCode, Intent data)
241 {
242 switch (requestCode)
243 {
244 case SELECT_TRUSTED_CERTIFICATE:
245 if (resultCode == RESULT_OK)
246 {
247 String alias = data.getStringExtra(VpnProfileDataSource.KEY_CERTIFICATE);
248 X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
249 mCertEntry = certificate == null ? null : new TrustedCertificateEntry(alias, certificate);
250 updateCertificateSelector();
251 }
252 break;
253 default:
254 super.onActivityResult(requestCode, resultCode, data);
255 }
256 }
257
258 /**
259 * Update the UI to enter credentials depending on the type of VPN currently selected
260 */
261 private void updateCredentialView()
262 {
263 mUsernamePassword.setVisibility(mVpnType.has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE);
264 mUserCertificate.setVisibility(mVpnType.has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE);
265 mTncNotice.setVisibility(mVpnType.has(VpnTypeFeature.BYOD) ? View.VISIBLE : View.GONE);
266
267 if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
268 {
269 if (mUserCertLoading != null)
270 {
271 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
272 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
273 }
274 else if (mUserCertEntry != null)
275 { /* clear any errors and set the new data */
276 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
277 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
278 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
279 }
280 else
281 {
282 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label);
283 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate);
284 }
285 }
286 }
287
288 /**
289 * Show an alert in case the previously selected certificate is not found anymore
290 * or the user did not select a certificate in the spinner.
291 */
292 private void showCertificateAlert()
293 {
294 AlertDialog.Builder adb = new AlertDialog.Builder(VpnProfileDetailActivity.this);
295 adb.setTitle(R.string.alert_text_nocertfound_title);
296 adb.setMessage(R.string.alert_text_nocertfound);
297 adb.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
298 @Override
299 public void onClick(DialogInterface dialog, int id)
300 {
301 dialog.cancel();
302 }
303 });
304 adb.show();
305 }
306
307 /**
308 * Update the CA certificate selection UI depending on whether the
309 * certificate should be automatically selected or not.
310 */
311 private void updateCertificateSelector()
312 {
313 if (!mCheckAuto.isChecked())
314 {
315 mSelectCert.setEnabled(true);
316 mSelectCert.setVisibility(View.VISIBLE);
317
318 if (mCertEntry != null)
319 {
320 ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
321 ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
322 }
323 else
324 {
325 ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(R.string.profile_ca_select_certificate_label);
326 ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(R.string.profile_ca_select_certificate);
327 }
328 }
329 else
330 {
331 mSelectCert.setEnabled(false);
332 mSelectCert.setVisibility(View.GONE);
333 }
334 }
335
336 /**
337 * Update the advanced settings UI depending on whether any advanced
338 * settings have already been made.
339 */
340 private void updateAdvancedSettings()
341 {
342 boolean show = mShowAdvanced.isChecked();
343 if (!show && mProfile != null)
344 {
345 show = mProfile.getMTU() != null;
346 }
347 mShowAdvanced.setVisibility(!show ? View.VISIBLE : View.GONE);
348 mAdvancedSettings.setVisibility(show ? View.VISIBLE : View.GONE);
349 }
350
351 /**
352 * Save or update the profile depending on whether we actually have a
353 * profile object or not (this was created in updateProfileData)
354 */
355 private void saveProfile()
356 {
357 if (verifyInput())
358 {
359 if (mProfile != null)
360 {
361 updateProfileData();
362 mDataSource.updateVpnProfile(mProfile);
363 }
364 else
365 {
366 mProfile = new VpnProfile();
367 updateProfileData();
368 mDataSource.insertProfile(mProfile);
369 }
370 setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
371 finish();
372 }
373 }
374
375 /**
376 * Verify the user input and display error messages.
377 * @return true if the input is valid
378 */
379 private boolean verifyInput()
380 {
381 boolean valid = true;
382 if (mGateway.getText().toString().trim().isEmpty())
383 {
384 mGateway.setError(getString(R.string.alert_text_no_input_gateway));
385 valid = false;
386 }
387 if (mVpnType.has(VpnTypeFeature.USER_PASS))
388 {
389 if (mUsername.getText().toString().trim().isEmpty())
390 {
391 mUsername.setError(getString(R.string.alert_text_no_input_username));
392 valid = false;
393 }
394 }
395 if (mVpnType.has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null)
396 { /* let's show an error icon */
397 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
398 valid = false;
399 }
400 if (!mCheckAuto.isChecked() && mCertEntry == null)
401 {
402 showCertificateAlert();
403 valid = false;
404 }
405 Integer mtu = getInteger(mMTU);
406 if (mtu != null && (mtu < MTU_MIN || mtu > MTU_MAX))
407 {
408 mMTU.setError(String.format(getString(R.string.alert_text_out_of_range), MTU_MIN, MTU_MAX));
409 valid = false;
410 }
411 return valid;
412 }
413
414 /**
415 * Update the profile object with the data entered by the user
416 */
417 private void updateProfileData()
418 {
419 /* the name is optional, we default to the gateway if none is given */
420 String name = mName.getText().toString().trim();
421 String gateway = mGateway.getText().toString().trim();
422 mProfile.setName(name.isEmpty() ? gateway : name);
423 mProfile.setGateway(gateway);
424 mProfile.setVpnType(mVpnType);
425 if (mVpnType.has(VpnTypeFeature.USER_PASS))
426 {
427 mProfile.setUsername(mUsername.getText().toString().trim());
428 String password = mPassword.getText().toString().trim();
429 password = password.isEmpty() ? null : password;
430 mProfile.setPassword(password);
431 }
432 if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
433 {
434 mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
435 }
436 String certAlias = mCheckAuto.isChecked() ? null : mCertEntry.getAlias();
437 mProfile.setCertificateAlias(certAlias);
438 mProfile.setMTU(getInteger(mMTU));
439 }
440
441 /**
442 * Load an existing profile if we got an ID
443 *
444 * @param savedInstanceState previously saved state
445 */
446 private void loadProfileData(Bundle savedInstanceState)
447 {
448 String useralias = null, alias = null;
449
450 getActionBar().setTitle(R.string.add_profile);
451 if (mId != null && mId != 0)
452 {
453 mProfile = mDataSource.getVpnProfile(mId);
454 if (mProfile != null)
455 {
456 mName.setText(mProfile.getName());
457 mGateway.setText(mProfile.getGateway());
458 mVpnType = mProfile.getVpnType();
459 mUsername.setText(mProfile.getUsername());
460 mPassword.setText(mProfile.getPassword());
461 mMTU.setText(mProfile.getMTU() != null ? mProfile.getMTU().toString() : null);
462 useralias = mProfile.getUserCertificateAlias();
463 alias = mProfile.getCertificateAlias();
464 getActionBar().setTitle(mProfile.getName());
465 }
466 else
467 {
468 Log.e(VpnProfileDetailActivity.class.getSimpleName(),
469 "VPN profile with id " + mId + " not found");
470 finish();
471 }
472 }
473
474 mSelectVpnType.setSelection(mVpnType.ordinal());
475
476 /* check if the user selected a user certificate previously */
477 useralias = savedInstanceState == null ? useralias: savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
478 if (useralias != null)
479 {
480 UserCertificateLoader loader = new UserCertificateLoader(this, useralias);
481 mUserCertLoading = useralias;
482 loader.execute();
483 }
484
485 /* check if the user selected a CA certificate previously */
486 alias = savedInstanceState == null ? alias : savedInstanceState.getString(VpnProfileDataSource.KEY_CERTIFICATE);
487 mCheckAuto.setChecked(alias == null);
488 if (alias != null)
489 {
490 X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
491 if (certificate != null)
492 {
493 mCertEntry = new TrustedCertificateEntry(alias, certificate);
494 }
495 else
496 { /* previously selected certificate is not here anymore */
497 showCertificateAlert();
498 mCertEntry = null;
499 }
500 }
501 }
502
503 /**
504 * Get the integer value in the given text box or null if empty
505 *
506 * @param view text box (numeric entry assumed)
507 */
508 private Integer getInteger(EditText view)
509 {
510 String value = view.getText().toString().trim();
511 return value.isEmpty() ? null : Integer.valueOf(value);
512 }
513
514 private class SelectUserCertOnClickListener implements OnClickListener, KeyChainAliasCallback
515 {
516 @Override
517 public void onClick(View v)
518 {
519 String useralias = mUserCertEntry != null ? mUserCertEntry.getAlias() : null;
520 KeyChain.choosePrivateKeyAlias(VpnProfileDetailActivity.this, this, new String[] { "RSA" }, null, null, -1, useralias);
521 }
522
523 @Override
524 public void alias(final String alias)
525 {
526 if (alias != null)
527 { /* otherwise the dialog was canceled, the request denied */
528 try
529 {
530 final X509Certificate[] chain = KeyChain.getCertificateChain(VpnProfileDetailActivity.this, alias);
531 /* alias() is not called from our main thread */
532 runOnUiThread(new Runnable() {
533 @Override
534 public void run()
535 {
536 if (chain != null && chain.length > 0)
537 {
538 mUserCertEntry = new TrustedCertificateEntry(alias, chain[0]);
539 }
540 updateCredentialView();
541 }
542 });
543 }
544 catch (KeyChainException e)
545 {
546 e.printStackTrace();
547 }
548 catch (InterruptedException e)
549 {
550 e.printStackTrace();
551 }
552 }
553 }
554 }
555
556 /**
557 * Load the selected user certificate asynchronously. This cannot be done
558 * from the main thread as getCertificateChain() calls back to our main
559 * thread to bind to the KeyChain service resulting in a deadlock.
560 */
561 private class UserCertificateLoader extends AsyncTask<Void, Void, X509Certificate>
562 {
563 private final Context mContext;
564 private final String mAlias;
565
566 public UserCertificateLoader(Context context, String alias)
567 {
568 mContext = context;
569 mAlias = alias;
570 }
571
572 @Override
573 protected X509Certificate doInBackground(Void... params)
574 {
575 X509Certificate[] chain = null;
576 try
577 {
578 chain = KeyChain.getCertificateChain(mContext, mAlias);
579 }
580 catch (KeyChainException e)
581 {
582 e.printStackTrace();
583 }
584 catch (InterruptedException e)
585 {
586 e.printStackTrace();
587 }
588 if (chain != null && chain.length > 0)
589 {
590 return chain[0];
591 }
592 return null;
593 }
594
595 @Override
596 protected void onPostExecute(X509Certificate result)
597 {
598 if (result != null)
599 {
600 mUserCertEntry = new TrustedCertificateEntry(mAlias, result);
601 }
602 else
603 { /* previously selected certificate is not here anymore */
604 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
605 mUserCertEntry = null;
606 }
607 mUserCertLoading = null;
608 updateCredentialView();
609 }
610 }
611
612 /**
613 * Dialog with notification message if EAP-TNC is used.
614 */
615 public static class TncNoticeDialog extends DialogFragment
616 {
617 @Override
618 public Dialog onCreateDialog(Bundle savedInstanceState)
619 {
620 return new AlertDialog.Builder(getActivity())
621 .setTitle(R.string.tnc_notice_title)
622 .setMessage(Html.fromHtml(getString(R.string.tnc_notice_details)))
623 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
624 @Override
625 public void onClick(DialogInterface dialog, int id)
626 {
627 dialog.dismiss();
628 }
629 }).create();
630 }
631 }
632 }