android: Make sending certificate requests configurable in the GUI
[strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / ui / VpnProfileDetailActivity.java
1 /*
2 * Copyright (C) 2012-2017 Tobias Brunner
3 * Copyright (C) 2012 Giuliano Grassi
4 * Copyright (C) 2012 Ralf Sager
5 * HSR 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.v4.content.LocalBroadcastManager;
30 import android.support.v7.app.AlertDialog;
31 import android.support.v7.app.AppCompatActivity;
32 import android.support.v7.app.AppCompatDialogFragment;
33 import android.text.Editable;
34 import android.text.Html;
35 import android.text.SpannableString;
36 import android.text.Spanned;
37 import android.text.TextUtils;
38 import android.text.TextWatcher;
39 import android.util.Log;
40 import android.view.Menu;
41 import android.view.MenuInflater;
42 import android.view.MenuItem;
43 import android.view.View;
44 import android.view.View.OnClickListener;
45 import android.view.ViewGroup;
46 import android.widget.AdapterView;
47 import android.widget.AdapterView.OnItemSelectedListener;
48 import android.widget.ArrayAdapter;
49 import android.widget.CheckBox;
50 import android.widget.CompoundButton;
51 import android.widget.CompoundButton.OnCheckedChangeListener;
52 import android.widget.EditText;
53 import android.widget.MultiAutoCompleteTextView;
54 import android.widget.RelativeLayout;
55 import android.widget.Spinner;
56 import android.widget.Switch;
57 import android.widget.TextView;
58
59 import org.strongswan.android.R;
60 import org.strongswan.android.data.VpnProfile;
61 import org.strongswan.android.data.VpnProfile.SelectedAppsHandling;
62 import org.strongswan.android.data.VpnProfileDataSource;
63 import org.strongswan.android.data.VpnType;
64 import org.strongswan.android.data.VpnType.VpnTypeFeature;
65 import org.strongswan.android.logic.TrustedCertificateManager;
66 import org.strongswan.android.security.TrustedCertificateEntry;
67 import org.strongswan.android.ui.adapter.CertificateIdentitiesAdapter;
68 import org.strongswan.android.ui.widget.TextInputLayoutHelper;
69 import org.strongswan.android.utils.Constants;
70 import org.strongswan.android.utils.IPRangeSet;
71
72 import java.security.cert.X509Certificate;
73 import java.util.ArrayList;
74 import java.util.SortedSet;
75 import java.util.TreeSet;
76 import java.util.UUID;
77
78 public class VpnProfileDetailActivity extends AppCompatActivity
79 {
80 private static final int SELECT_TRUSTED_CERTIFICATE = 0;
81 private static final int SELECT_APPLICATIONS = 1;
82
83 private VpnProfileDataSource mDataSource;
84 private Long mId;
85 private TrustedCertificateEntry mCertEntry;
86 private String mUserCertLoading;
87 private CertificateIdentitiesAdapter mSelectUserIdAdapter;
88 private String mSelectedUserId;
89 private TrustedCertificateEntry mUserCertEntry;
90 private VpnType mVpnType = VpnType.IKEV2_EAP;
91 private SelectedAppsHandling mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE;
92 private SortedSet<String> mSelectedApps = new TreeSet<>();
93 private VpnProfile mProfile;
94 private MultiAutoCompleteTextView mName;
95 private TextInputLayoutHelper mNameWrap;
96 private EditText mGateway;
97 private TextInputLayoutHelper mGatewayWrap;
98 private Spinner mSelectVpnType;
99 private ViewGroup mUsernamePassword;
100 private EditText mUsername;
101 private TextInputLayoutHelper mUsernameWrap;
102 private EditText mPassword;
103 private ViewGroup mUserCertificate;
104 private RelativeLayout mSelectUserCert;
105 private Spinner mSelectUserId;
106 private CheckBox mCheckAuto;
107 private RelativeLayout mSelectCert;
108 private RelativeLayout mTncNotice;
109 private CheckBox mShowAdvanced;
110 private ViewGroup mAdvancedSettings;
111 private MultiAutoCompleteTextView mRemoteId;
112 private TextInputLayoutHelper mRemoteIdWrap;
113 private EditText mMTU;
114 private TextInputLayoutHelper mMTUWrap;
115 private EditText mPort;
116 private TextInputLayoutHelper mPortWrap;
117 private Switch mCertReq;
118 private EditText mNATKeepalive;
119 private TextInputLayoutHelper mNATKeepaliveWrap;
120 private EditText mIncludedSubnets;
121 private TextInputLayoutHelper mIncludedSubnetsWrap;
122 private EditText mExcludedSubnets;
123 private TextInputLayoutHelper mExcludedSubnetsWrap;
124 private CheckBox mBlockIPv4;
125 private CheckBox mBlockIPv6;
126 private Spinner mSelectSelectedAppsHandling;
127 private RelativeLayout mSelectApps;
128
129 @Override
130 public void onCreate(Bundle savedInstanceState)
131 {
132 super.onCreate(savedInstanceState);
133
134 /* the title is set when we load the profile, if any */
135 getSupportActionBar().setDisplayHomeAsUpEnabled(true);
136
137 mDataSource = new VpnProfileDataSource(this);
138 mDataSource.open();
139
140 setContentView(R.layout.profile_detail_view);
141
142 mName = (MultiAutoCompleteTextView)findViewById(R.id.name);
143 mNameWrap = (TextInputLayoutHelper)findViewById(R.id.name_wrap);
144 mGateway = (EditText)findViewById(R.id.gateway);
145 mGatewayWrap = (TextInputLayoutHelper) findViewById(R.id.gateway_wrap);
146 mSelectVpnType = (Spinner)findViewById(R.id.vpn_type);
147 mTncNotice = (RelativeLayout)findViewById(R.id.tnc_notice);
148
149 mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
150 mUsername = (EditText)findViewById(R.id.username);
151 mUsernameWrap = (TextInputLayoutHelper) findViewById(R.id.username_wrap);
152 mPassword = (EditText)findViewById(R.id.password);
153
154 mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group);
155 mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate);
156 mSelectUserId = (Spinner)findViewById(R.id.select_user_id);
157
158 mCheckAuto = (CheckBox)findViewById(R.id.ca_auto);
159 mSelectCert = (RelativeLayout)findViewById(R.id.select_certificate);
160
161 mShowAdvanced = (CheckBox)findViewById(R.id.show_advanced);
162 mAdvancedSettings = (ViewGroup)findViewById(R.id.advanced_settings);
163
164 mRemoteId = (MultiAutoCompleteTextView)findViewById(R.id.remote_id);
165 mRemoteIdWrap = (TextInputLayoutHelper) findViewById(R.id.remote_id_wrap);
166 mMTU = (EditText)findViewById(R.id.mtu);
167 mMTUWrap = (TextInputLayoutHelper) findViewById(R.id.mtu_wrap);
168 mPort = (EditText)findViewById(R.id.port);
169 mPortWrap = (TextInputLayoutHelper) findViewById(R.id.port_wrap);
170 mNATKeepalive = (EditText)findViewById(R.id.nat_keepalive);
171 mNATKeepaliveWrap = (TextInputLayoutHelper) findViewById(R.id.nat_keepalive_wrap);
172 mCertReq = (Switch)findViewById(R.id.cert_req);
173 mIncludedSubnets = (EditText)findViewById(R.id.included_subnets);
174 mIncludedSubnetsWrap = (TextInputLayoutHelper)findViewById(R.id.included_subnets_wrap);
175 mExcludedSubnets = (EditText)findViewById(R.id.excluded_subnets);
176 mExcludedSubnetsWrap = (TextInputLayoutHelper)findViewById(R.id.excluded_subnets_wrap);
177 mBlockIPv4 = (CheckBox)findViewById(R.id.split_tunneling_v4);
178 mBlockIPv6 = (CheckBox)findViewById(R.id.split_tunneling_v6);
179
180 mSelectSelectedAppsHandling = (Spinner)findViewById(R.id.apps_handling);
181 mSelectApps = (RelativeLayout)findViewById(R.id.select_applications);
182
183 final SpaceTokenizer spaceTokenizer = new SpaceTokenizer();
184 mName.setTokenizer(spaceTokenizer);
185 mRemoteId.setTokenizer(spaceTokenizer);
186 final ArrayAdapter<String> completeAdapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line);
187 mName.setAdapter(completeAdapter);
188 mRemoteId.setAdapter(completeAdapter);
189
190 mGateway.addTextChangedListener(new TextWatcher() {
191 @Override
192 public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
193
194 @Override
195 public void onTextChanged(CharSequence s, int start, int before, int count) {}
196
197 @Override
198 public void afterTextChanged(Editable s)
199 {
200 completeAdapter.clear();
201 completeAdapter.add(mGateway.getText().toString());
202 if (TextUtils.isEmpty(mGateway.getText()))
203 {
204 mNameWrap.setHelperText(getString(R.string.profile_name_hint));
205 mRemoteIdWrap.setHelperText(getString(R.string.profile_remote_id_hint));
206 }
207 else
208 {
209 mNameWrap.setHelperText(String.format(getString(R.string.profile_name_hint_gateway), mGateway.getText()));
210 mRemoteIdWrap.setHelperText(String.format(getString(R.string.profile_remote_id_hint_gateway), mGateway.getText()));
211 }
212 }
213 });
214
215 mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() {
216 @Override
217 public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
218 {
219 mVpnType = VpnType.values()[position];
220 updateCredentialView();
221 }
222
223 @Override
224 public void onNothingSelected(AdapterView<?> parent)
225 { /* should not happen */
226 mVpnType = VpnType.IKEV2_EAP;
227 updateCredentialView();
228 }
229 });
230
231 ((TextView)mTncNotice.findViewById(android.R.id.text1)).setText(R.string.tnc_notice_title);
232 ((TextView)mTncNotice.findViewById(android.R.id.text2)).setText(R.string.tnc_notice_subtitle);
233 mTncNotice.setOnClickListener(new OnClickListener() {
234 @Override
235 public void onClick(View v)
236 {
237 new TncNoticeDialog().show(VpnProfileDetailActivity.this.getSupportFragmentManager(), "TncNotice");
238 }
239 });
240
241 mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
242 mSelectUserIdAdapter = new CertificateIdentitiesAdapter(this);
243 mSelectUserId.setAdapter(mSelectUserIdAdapter);
244 mSelectUserId.setOnItemSelectedListener(new OnItemSelectedListener() {
245 @Override
246 public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
247 {
248 if (mUserCertEntry != null)
249 { /* we don't store the subject DN as it is in the reverse order and the default anyway */
250 mSelectedUserId = position == 0 ? null : mSelectUserIdAdapter.getItem(position);
251 }
252 }
253
254 @Override
255 public void onNothingSelected(AdapterView<?> parent)
256 {
257 mSelectedUserId = null;
258 }
259 });
260
261 mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() {
262 @Override
263 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
264 {
265 updateCertificateSelector();
266 }
267 });
268
269 mSelectCert.setOnClickListener(new OnClickListener() {
270 @Override
271 public void onClick(View v)
272 {
273 Intent intent = new Intent(VpnProfileDetailActivity.this, TrustedCertificatesActivity.class);
274 intent.setAction(TrustedCertificatesActivity.SELECT_CERTIFICATE);
275 startActivityForResult(intent, SELECT_TRUSTED_CERTIFICATE);
276 }
277 });
278
279 mShowAdvanced.setOnCheckedChangeListener(new OnCheckedChangeListener() {
280 @Override
281 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked)
282 {
283 updateAdvancedSettings();
284 }
285 });
286
287 mSelectSelectedAppsHandling.setOnItemSelectedListener(new OnItemSelectedListener() {
288 @Override
289 public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
290 {
291 mSelectedAppsHandling = SelectedAppsHandling.values()[position];
292 updateAppsSelector();
293 }
294
295 @Override
296 public void onNothingSelected(AdapterView<?> parent)
297 { /* should not happen */
298 mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE;
299 updateAppsSelector();
300 }
301 });
302
303 mSelectApps.setOnClickListener(new OnClickListener() {
304 @Override
305 public void onClick(View v)
306 {
307 Intent intent = new Intent(VpnProfileDetailActivity.this, SelectedApplicationsActivity.class);
308 intent.putExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelectedApps));
309 startActivityForResult(intent, SELECT_APPLICATIONS);
310 }
311 });
312
313 mId = savedInstanceState == null ? null : savedInstanceState.getLong(VpnProfileDataSource.KEY_ID);
314 if (mId == null)
315 {
316 Bundle extras = getIntent().getExtras();
317 mId = extras == null ? null : extras.getLong(VpnProfileDataSource.KEY_ID);
318 }
319
320 loadProfileData(savedInstanceState);
321
322 updateCredentialView();
323 updateCertificateSelector();
324 updateAdvancedSettings();
325 updateAppsSelector();
326 }
327
328 @Override
329 protected void onDestroy()
330 {
331 super.onDestroy();
332 mDataSource.close();
333 }
334
335 @Override
336 protected void onSaveInstanceState(Bundle outState)
337 {
338 super.onSaveInstanceState(outState);
339 if (mId != null)
340 {
341 outState.putLong(VpnProfileDataSource.KEY_ID, mId);
342 }
343 if (mUserCertEntry != null)
344 {
345 outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
346 }
347 if (mSelectedUserId != null)
348 {
349 outState.putString(VpnProfileDataSource.KEY_LOCAL_ID, mSelectedUserId);
350 }
351 if (mCertEntry != null)
352 {
353 outState.putString(VpnProfileDataSource.KEY_CERTIFICATE, mCertEntry.getAlias());
354 }
355 outState.putStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelectedApps));
356 }
357
358 @Override
359 public boolean onCreateOptionsMenu(Menu menu)
360 {
361 MenuInflater inflater = getMenuInflater();
362 inflater.inflate(R.menu.profile_edit, menu);
363 return true;
364 }
365
366 @Override
367 public boolean onOptionsItemSelected(MenuItem item)
368 {
369 switch (item.getItemId())
370 {
371 case android.R.id.home:
372 case R.id.menu_cancel:
373 finish();
374 return true;
375 case R.id.menu_accept:
376 saveProfile();
377 return true;
378 default:
379 return super.onOptionsItemSelected(item);
380 }
381 }
382
383 @Override
384 protected void onActivityResult(int requestCode, int resultCode, Intent data)
385 {
386 switch (requestCode)
387 {
388 case SELECT_TRUSTED_CERTIFICATE:
389 if (resultCode == RESULT_OK)
390 {
391 String alias = data.getStringExtra(VpnProfileDataSource.KEY_CERTIFICATE);
392 X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
393 mCertEntry = certificate == null ? null : new TrustedCertificateEntry(alias, certificate);
394 updateCertificateSelector();
395 }
396 break;
397 case SELECT_APPLICATIONS:
398 if (resultCode == RESULT_OK)
399 {
400 ArrayList<String> selection = data.getStringArrayListExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
401 mSelectedApps = new TreeSet<>(selection);
402 updateAppsSelector();
403 }
404 break;
405 default:
406 super.onActivityResult(requestCode, resultCode, data);
407 }
408 }
409
410 /**
411 * Update the UI to enter credentials depending on the type of VPN currently selected
412 */
413 private void updateCredentialView()
414 {
415 mUsernamePassword.setVisibility(mVpnType.has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE);
416 mUserCertificate.setVisibility(mVpnType.has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE);
417 mTncNotice.setVisibility(mVpnType.has(VpnTypeFeature.BYOD) ? View.VISIBLE : View.GONE);
418
419 if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
420 {
421 mSelectUserId.setEnabled(false);
422 if (mUserCertLoading != null)
423 {
424 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
425 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
426 }
427 else if (mUserCertEntry != null)
428 { /* clear any errors and set the new data */
429 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
430 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
431 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
432 mSelectUserIdAdapter.setCertificate(mUserCertEntry);
433 mSelectUserId.setSelection(mSelectUserIdAdapter.getPosition(mSelectedUserId));
434 mSelectUserId.setEnabled(true);
435 }
436 else
437 {
438 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label);
439 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate);
440 mSelectUserIdAdapter.setCertificate(null);
441 }
442 }
443 }
444
445 /**
446 * Show an alert in case the previously selected certificate is not found anymore
447 * or the user did not select a certificate in the spinner.
448 */
449 private void showCertificateAlert()
450 {
451 AlertDialog.Builder adb = new AlertDialog.Builder(VpnProfileDetailActivity.this);
452 adb.setTitle(R.string.alert_text_nocertfound_title);
453 adb.setMessage(R.string.alert_text_nocertfound);
454 adb.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
455 @Override
456 public void onClick(DialogInterface dialog, int id)
457 {
458 dialog.cancel();
459 }
460 });
461 adb.show();
462 }
463
464 /**
465 * Update the CA certificate selection UI depending on whether the
466 * certificate should be automatically selected or not.
467 */
468 private void updateCertificateSelector()
469 {
470 if (!mCheckAuto.isChecked())
471 {
472 mSelectCert.setEnabled(true);
473 mSelectCert.setVisibility(View.VISIBLE);
474
475 if (mCertEntry != null)
476 {
477 ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
478 ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
479 }
480 else
481 {
482 ((TextView)mSelectCert.findViewById(android.R.id.text1)).setText(R.string.profile_ca_select_certificate_label);
483 ((TextView)mSelectCert.findViewById(android.R.id.text2)).setText(R.string.profile_ca_select_certificate);
484 }
485 }
486 else
487 {
488 mSelectCert.setEnabled(false);
489 mSelectCert.setVisibility(View.GONE);
490 }
491 }
492
493 /**
494 * Update the application selection UI
495 */
496 private void updateAppsSelector()
497 {
498 if (mSelectedAppsHandling == SelectedAppsHandling.SELECTED_APPS_DISABLE)
499 {
500 mSelectApps.setEnabled(false);
501 mSelectApps.setVisibility(View.GONE);
502
503 }
504 else
505 {
506 mSelectApps.setEnabled(true);
507 mSelectApps.setVisibility(View.VISIBLE);
508
509 ((TextView)mSelectApps.findViewById(android.R.id.text1)).setText(R.string.profile_select_apps);
510 String selected;
511 switch (mSelectedApps.size())
512 {
513 case 0:
514 selected = getString(R.string.profile_select_no_apps);
515 break;
516 case 1:
517 selected = getString(R.string.profile_select_one_app);
518 break;
519 default:
520 selected = getString(R.string.profile_select_x_apps, mSelectedApps.size());
521 break;
522 }
523 ((TextView)mSelectApps.findViewById(android.R.id.text2)).setText(selected);
524 }
525 }
526
527 /**
528 * Update the advanced settings UI depending on whether any advanced
529 * settings have already been made.
530 */
531 private void updateAdvancedSettings()
532 {
533 boolean show = mShowAdvanced.isChecked();
534 if (!show && mProfile != null)
535 {
536 Integer st = mProfile.getSplitTunneling(), flags = mProfile.getFlags();
537 show = mProfile.getRemoteId() != null || mProfile.getMTU() != null ||
538 mProfile.getPort() != null || mProfile.getNATKeepAlive() != null ||
539 (flags != null && flags != 0) || (st != null && st != 0) ||
540 mProfile.getIncludedSubnets() != null || mProfile.getExcludedSubnets() != null ||
541 mProfile.getSelectedAppsHandling() != SelectedAppsHandling.SELECTED_APPS_DISABLE;
542 }
543 mShowAdvanced.setVisibility(!show ? View.VISIBLE : View.GONE);
544 mAdvancedSettings.setVisibility(show ? View.VISIBLE : View.GONE);
545 }
546
547 /**
548 * Save or update the profile depending on whether we actually have a
549 * profile object or not (this was created in updateProfileData)
550 */
551 private void saveProfile()
552 {
553 if (verifyInput())
554 {
555 if (mProfile != null)
556 {
557 updateProfileData();
558 if (mProfile.getUUID() == null)
559 {
560 mProfile.setUUID(UUID.randomUUID());
561 }
562 mDataSource.updateVpnProfile(mProfile);
563 }
564 else
565 {
566 mProfile = new VpnProfile();
567 updateProfileData();
568 mDataSource.insertProfile(mProfile);
569 }
570 Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED);
571 intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId());
572 LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
573
574 setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
575 finish();
576 }
577 }
578
579 /**
580 * Verify the user input and display error messages.
581 * @return true if the input is valid
582 */
583 private boolean verifyInput()
584 {
585 boolean valid = true;
586 if (mGateway.getText().toString().trim().isEmpty())
587 {
588 mGatewayWrap.setError(getString(R.string.alert_text_no_input_gateway));
589 valid = false;
590 }
591 if (mVpnType.has(VpnTypeFeature.USER_PASS))
592 {
593 if (mUsername.getText().toString().trim().isEmpty())
594 {
595 mUsernameWrap.setError(getString(R.string.alert_text_no_input_username));
596 valid = false;
597 }
598 }
599 if (mVpnType.has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null)
600 { /* let's show an error icon */
601 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
602 valid = false;
603 }
604 if (!mCheckAuto.isChecked() && mCertEntry == null)
605 {
606 showCertificateAlert();
607 valid = false;
608 }
609 if (!validateInteger(mMTU, Constants.MTU_MIN, Constants.MTU_MAX))
610 {
611 mMTUWrap.setError(String.format(getString(R.string.alert_text_out_of_range), Constants.MTU_MIN, Constants.MTU_MAX));
612 valid = false;
613 }
614 if (!validateSubnets(mIncludedSubnets))
615 {
616 mIncludedSubnetsWrap.setError(getString(R.string.alert_text_no_subnets));
617 valid = false;
618 }
619 if (!validateSubnets(mExcludedSubnets))
620 {
621 mExcludedSubnetsWrap.setError(getString(R.string.alert_text_no_subnets));
622 valid = false;
623 }
624 if (!validateInteger(mPort, 1, 65535))
625 {
626 mPortWrap.setError(String.format(getString(R.string.alert_text_out_of_range), 1, 65535));
627 valid = false;
628 }
629 if (!validateInteger(mNATKeepalive, Constants.NAT_KEEPALIVE_MIN, Constants.NAT_KEEPALIVE_MAX))
630 {
631 mNATKeepaliveWrap.setError(String.format(getString(R.string.alert_text_out_of_range),
632 Constants.NAT_KEEPALIVE_MIN, Constants.NAT_KEEPALIVE_MAX));
633 valid = false;
634 }
635 return valid;
636 }
637
638 /**
639 * Update the profile object with the data entered by the user
640 */
641 private void updateProfileData()
642 {
643 /* the name is optional, we default to the gateway if none is given */
644 String name = mName.getText().toString().trim();
645 String gateway = mGateway.getText().toString().trim();
646 mProfile.setName(name.isEmpty() ? gateway : name);
647 mProfile.setGateway(gateway);
648 mProfile.setVpnType(mVpnType);
649 if (mVpnType.has(VpnTypeFeature.USER_PASS))
650 {
651 mProfile.setUsername(mUsername.getText().toString().trim());
652 String password = mPassword.getText().toString().trim();
653 password = password.isEmpty() ? null : password;
654 mProfile.setPassword(password);
655 }
656 if (mVpnType.has(VpnTypeFeature.CERTIFICATE))
657 {
658 mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
659 mProfile.setLocalId(mSelectedUserId);
660 }
661 String certAlias = mCheckAuto.isChecked() ? null : mCertEntry.getAlias();
662 mProfile.setCertificateAlias(certAlias);
663 String remote_id = mRemoteId.getText().toString().trim();
664 mProfile.setRemoteId(remote_id.isEmpty() ? null : remote_id);
665 mProfile.setMTU(getInteger(mMTU));
666 mProfile.setPort(getInteger(mPort));
667 mProfile.setNATKeepAlive(getInteger(mNATKeepalive));
668 int flags = 0;
669 flags |= !mCertReq.isChecked() ? VpnProfile.FLAGS_SUPPRESS_CERT_REQS : 0;
670 mProfile.setFlags(flags);
671 String included = mIncludedSubnets.getText().toString().trim();
672 mProfile.setIncludedSubnets(included.isEmpty() ? null : included);
673 String excluded = mExcludedSubnets.getText().toString().trim();
674 mProfile.setExcludedSubnets(excluded.isEmpty() ? null : excluded);
675 int st = 0;
676 st |= mBlockIPv4.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
677 st |= mBlockIPv6.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
678 mProfile.setSplitTunneling(st == 0 ? null : st);
679 mProfile.setSelectedAppsHandling(mSelectedAppsHandling);
680 mProfile.setSelectedApps(mSelectedApps);
681 }
682
683 /**
684 * Load an existing profile if we got an ID
685 *
686 * @param savedInstanceState previously saved state
687 */
688 private void loadProfileData(Bundle savedInstanceState)
689 {
690 String useralias = null, local_id = null, alias = null;
691 Integer flags = null;
692
693 getSupportActionBar().setTitle(R.string.add_profile);
694 if (mId != null && mId != 0)
695 {
696 mProfile = mDataSource.getVpnProfile(mId);
697 if (mProfile != null)
698 {
699 mName.setText(mProfile.getName());
700 mGateway.setText(mProfile.getGateway());
701 mVpnType = mProfile.getVpnType();
702 mUsername.setText(mProfile.getUsername());
703 mPassword.setText(mProfile.getPassword());
704 mRemoteId.setText(mProfile.getRemoteId());
705 mMTU.setText(mProfile.getMTU() != null ? mProfile.getMTU().toString() : null);
706 mPort.setText(mProfile.getPort() != null ? mProfile.getPort().toString() : null);
707 mNATKeepalive.setText(mProfile.getNATKeepAlive() != null ? mProfile.getNATKeepAlive().toString() : null);
708 mIncludedSubnets.setText(mProfile.getIncludedSubnets());
709 mExcludedSubnets.setText(mProfile.getExcludedSubnets());
710 mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0);
711 mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0);
712 mSelectedAppsHandling = mProfile.getSelectedAppsHandling();
713 mSelectedApps = mProfile.getSelectedAppsSet();
714 flags = mProfile.getFlags();
715 useralias = mProfile.getUserCertificateAlias();
716 local_id = mProfile.getLocalId();
717 alias = mProfile.getCertificateAlias();
718 getSupportActionBar().setTitle(mProfile.getName());
719 }
720 else
721 {
722 Log.e(VpnProfileDetailActivity.class.getSimpleName(),
723 "VPN profile with id " + mId + " not found");
724 finish();
725 }
726 }
727
728 mSelectVpnType.setSelection(mVpnType.ordinal());
729 mCertReq.setChecked(flags == null || (flags & VpnProfile.FLAGS_SUPPRESS_CERT_REQS) == 0);
730
731 /* check if the user selected a user certificate previously */
732 useralias = savedInstanceState == null ? useralias : savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
733 local_id = savedInstanceState == null ? local_id : savedInstanceState.getString(VpnProfileDataSource.KEY_LOCAL_ID);
734 if (useralias != null)
735 {
736 UserCertificateLoader loader = new UserCertificateLoader(this, useralias);
737 mUserCertLoading = useralias;
738 mSelectedUserId = local_id;
739 loader.execute();
740 }
741
742 /* check if the user selected a CA certificate previously */
743 alias = savedInstanceState == null ? alias : savedInstanceState.getString(VpnProfileDataSource.KEY_CERTIFICATE);
744 mCheckAuto.setChecked(alias == null);
745 if (alias != null)
746 {
747 X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
748 if (certificate != null)
749 {
750 mCertEntry = new TrustedCertificateEntry(alias, certificate);
751 }
752 else
753 { /* previously selected certificate is not here anymore */
754 showCertificateAlert();
755 mCertEntry = null;
756 }
757 }
758
759 mSelectSelectedAppsHandling.setSelection(mSelectedAppsHandling.ordinal());
760 if (savedInstanceState != null)
761 {
762 ArrayList<String> selectedApps = savedInstanceState.getStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
763 mSelectedApps = new TreeSet<>(selectedApps);
764 }
765 }
766
767 /**
768 * Get the integer value in the given text box or null if empty
769 *
770 * @param view text box (numeric entry assumed)
771 */
772 private Integer getInteger(EditText view)
773 {
774 String value = view.getText().toString().trim();
775 try
776 {
777 return value.isEmpty() ? null : Integer.valueOf(value);
778 }
779 catch (NumberFormatException e)
780 {
781 return null;
782 }
783 }
784
785 /**
786 * Check that the value in the given text box is a valid integer in the given range
787 *
788 * @param view text box (numeric entry assumed)
789 * @param min minimum value (inclusive)
790 * @param max maximum value (inclusive)
791 */
792 private boolean validateInteger(EditText view, Integer min, Integer max)
793 {
794 String value = view.getText().toString().trim();
795 try
796 {
797 if (value.isEmpty())
798 {
799 return true;
800 }
801 Integer val = Integer.valueOf(value);
802 return min <= val && val <= max;
803 }
804 catch (NumberFormatException e)
805 {
806 return false;
807 }
808 }
809
810 /**
811 * Check that the value in the given text box is a valid list of subnets/ranges
812 *
813 * @param view text box
814 */
815 private boolean validateSubnets(EditText view)
816 {
817 String value = view.getText().toString().trim();
818 return value.isEmpty() || IPRangeSet.fromString(value) != null;
819 }
820
821 private class SelectUserCertOnClickListener implements OnClickListener, KeyChainAliasCallback
822 {
823 @Override
824 public void onClick(View v)
825 {
826 String useralias = mUserCertEntry != null ? mUserCertEntry.getAlias() : null;
827 KeyChain.choosePrivateKeyAlias(VpnProfileDetailActivity.this, this, new String[] { "RSA" }, null, null, -1, useralias);
828 }
829
830 @Override
831 public void alias(final String alias)
832 {
833 if (alias != null)
834 { /* otherwise the dialog was canceled, the request denied */
835 try
836 {
837 final X509Certificate[] chain = KeyChain.getCertificateChain(VpnProfileDetailActivity.this, alias);
838 /* alias() is not called from our main thread */
839 runOnUiThread(new Runnable() {
840 @Override
841 public void run()
842 {
843 if (chain != null && chain.length > 0)
844 {
845 mUserCertEntry = new TrustedCertificateEntry(alias, chain[0]);
846 }
847 updateCredentialView();
848 }
849 });
850 }
851 catch (KeyChainException | InterruptedException e)
852 {
853 e.printStackTrace();
854 }
855 }
856 }
857 }
858
859 /**
860 * Load the selected user certificate asynchronously. This cannot be done
861 * from the main thread as getCertificateChain() calls back to our main
862 * thread to bind to the KeyChain service resulting in a deadlock.
863 */
864 private class UserCertificateLoader extends AsyncTask<Void, Void, X509Certificate>
865 {
866 private final Context mContext;
867 private final String mAlias;
868
869 public UserCertificateLoader(Context context, String alias)
870 {
871 mContext = context;
872 mAlias = alias;
873 }
874
875 @Override
876 protected X509Certificate doInBackground(Void... params)
877 {
878 X509Certificate[] chain = null;
879 try
880 {
881 chain = KeyChain.getCertificateChain(mContext, mAlias);
882 }
883 catch (KeyChainException | InterruptedException e)
884 {
885 e.printStackTrace();
886 }
887 if (chain != null && chain.length > 0)
888 {
889 return chain[0];
890 }
891 return null;
892 }
893
894 @Override
895 protected void onPostExecute(X509Certificate result)
896 {
897 if (result != null)
898 {
899 mUserCertEntry = new TrustedCertificateEntry(mAlias, result);
900 }
901 else
902 { /* previously selected certificate is not here anymore */
903 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
904 mUserCertEntry = null;
905 }
906 mUserCertLoading = null;
907 updateCredentialView();
908 }
909 }
910
911 /**
912 * Dialog with notification message if EAP-TNC is used.
913 */
914 public static class TncNoticeDialog extends AppCompatDialogFragment
915 {
916 @Override
917 public Dialog onCreateDialog(Bundle savedInstanceState)
918 {
919 return new AlertDialog.Builder(getActivity())
920 .setTitle(R.string.tnc_notice_title)
921 .setMessage(Html.fromHtml(getString(R.string.tnc_notice_details)))
922 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
923 @Override
924 public void onClick(DialogInterface dialog, int id)
925 {
926 dialog.dismiss();
927 }
928 }).create();
929 }
930 }
931
932 /**
933 * Tokenizer implementation that separates by white-space
934 */
935 public static class SpaceTokenizer implements MultiAutoCompleteTextView.Tokenizer
936 {
937 @Override
938 public int findTokenStart(CharSequence text, int cursor)
939 {
940 int i = cursor;
941
942 while (i > 0 && !Character.isWhitespace(text.charAt(i - 1)))
943 {
944 i--;
945 }
946 return i;
947 }
948
949 @Override
950 public int findTokenEnd(CharSequence text, int cursor)
951 {
952 int i = cursor;
953 int len = text.length();
954
955 while (i < len)
956 {
957 if (Character.isWhitespace(text.charAt(i)))
958 {
959 return i;
960 }
961 else
962 {
963 i++;
964 }
965 }
966 return len;
967 }
968
969 @Override
970 public CharSequence terminateToken(CharSequence text)
971 {
972 int i = text.length();
973
974 if (i > 0 && Character.isWhitespace(text.charAt(i - 1)))
975 {
976 return text;
977 }
978 else
979 {
980 if (text instanceof Spanned)
981 {
982 SpannableString sp = new SpannableString(text + " ");
983 TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, sp, 0);
984 return sp;
985 }
986 else
987 {
988 return text + " ";
989 }
990 }
991 }
992 }
993 }