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