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