2 * Copyright (C) 2012-2016 Tobias Brunner
3 * Copyright (C) 2012 Giuliano Grassi
4 * Copyright (C) 2012 Ralf Sager
5 * HSR Hochschule fuer Technik Rapperswil
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>.
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
18 package org
.strongswan
.android
.ui
;
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
.Editable
;
33 import android
.text
.Html
;
34 import android
.text
.TextUtils
;
35 import android
.text
.TextWatcher
;
36 import android
.util
.Log
;
37 import android
.view
.Menu
;
38 import android
.view
.MenuInflater
;
39 import android
.view
.MenuItem
;
40 import android
.view
.View
;
41 import android
.view
.View
.OnClickListener
;
42 import android
.view
.ViewGroup
;
43 import android
.widget
.AdapterView
;
44 import android
.widget
.AdapterView
.OnItemSelectedListener
;
45 import android
.widget
.CheckBox
;
46 import android
.widget
.CompoundButton
;
47 import android
.widget
.CompoundButton
.OnCheckedChangeListener
;
48 import android
.widget
.EditText
;
49 import android
.widget
.RelativeLayout
;
50 import android
.widget
.Spinner
;
51 import android
.widget
.TextView
;
53 import org
.strongswan
.android
.R
;
54 import org
.strongswan
.android
.data
.VpnProfile
;
55 import org
.strongswan
.android
.data
.VpnProfileDataSource
;
56 import org
.strongswan
.android
.data
.VpnType
;
57 import org
.strongswan
.android
.data
.VpnType
.VpnTypeFeature
;
58 import org
.strongswan
.android
.logic
.TrustedCertificateManager
;
59 import org
.strongswan
.android
.security
.TrustedCertificateEntry
;
60 import org
.strongswan
.android
.ui
.widget
.TextInputLayoutHelper
;
62 import java
.security
.cert
.X509Certificate
;
64 public class VpnProfileDetailActivity
extends AppCompatActivity
66 private static final int SELECT_TRUSTED_CERTIFICATE
= 0;
67 private static final int MTU_MIN
= 1280;
68 private static final int MTU_MAX
= 1500;
70 private VpnProfileDataSource mDataSource
;
72 private TrustedCertificateEntry mCertEntry
;
73 private String mUserCertLoading
;
74 private TrustedCertificateEntry mUserCertEntry
;
75 private VpnType mVpnType
= VpnType
.IKEV2_EAP
;
76 private VpnProfile mProfile
;
77 private EditText mName
;
78 private TextInputLayoutHelper mNameWrap
;
79 private EditText mGateway
;
80 private TextInputLayoutHelper mGatewayWrap
;
81 private Spinner mSelectVpnType
;
82 private ViewGroup mUsernamePassword
;
83 private EditText mUsername
;
84 private TextInputLayoutHelper mUsernameWrap
;
85 private EditText mPassword
;
86 private ViewGroup mUserCertificate
;
87 private RelativeLayout mSelectUserCert
;
88 private CheckBox mCheckAuto
;
89 private RelativeLayout mSelectCert
;
90 private RelativeLayout mTncNotice
;
91 private CheckBox mShowAdvanced
;
92 private ViewGroup mAdvancedSettings
;
93 private EditText mMTU
;
94 private TextInputLayoutHelper mMTUWrap
;
95 private EditText mPort
;
96 private TextInputLayoutHelper mPortWrap
;
97 private CheckBox mBlockIPv4
;
98 private CheckBox mBlockIPv6
;
101 public void onCreate(Bundle savedInstanceState
)
103 super.onCreate(savedInstanceState
);
105 /* the title is set when we load the profile, if any */
106 getSupportActionBar().setDisplayHomeAsUpEnabled(true
);
108 mDataSource
= new VpnProfileDataSource(this);
111 setContentView(R
.layout
.profile_detail_view
);
113 mName
= (EditText
)findViewById(R
.id
.name
);
114 mNameWrap
= (TextInputLayoutHelper
)findViewById(R
.id
.name_wrap
);
115 mGateway
= (EditText
)findViewById(R
.id
.gateway
);
116 mGatewayWrap
= (TextInputLayoutHelper
) findViewById(R
.id
.gateway_wrap
);
117 mSelectVpnType
= (Spinner
)findViewById(R
.id
.vpn_type
);
118 mTncNotice
= (RelativeLayout
)findViewById(R
.id
.tnc_notice
);
120 mUsernamePassword
= (ViewGroup
)findViewById(R
.id
.username_password_group
);
121 mUsername
= (EditText
)findViewById(R
.id
.username
);
122 mUsernameWrap
= (TextInputLayoutHelper
) findViewById(R
.id
.username_wrap
);
123 mPassword
= (EditText
)findViewById(R
.id
.password
);
125 mUserCertificate
= (ViewGroup
)findViewById(R
.id
.user_certificate_group
);
126 mSelectUserCert
= (RelativeLayout
)findViewById(R
.id
.select_user_certificate
);
128 mCheckAuto
= (CheckBox
)findViewById(R
.id
.ca_auto
);
129 mSelectCert
= (RelativeLayout
)findViewById(R
.id
.select_certificate
);
131 mShowAdvanced
= (CheckBox
)findViewById(R
.id
.show_advanced
);
132 mAdvancedSettings
= (ViewGroup
)findViewById(R
.id
.advanced_settings
);
134 mMTU
= (EditText
)findViewById(R
.id
.mtu
);
135 mMTUWrap
= (TextInputLayoutHelper
) findViewById(R
.id
.mtu_wrap
);
136 mPort
= (EditText
)findViewById(R
.id
.port
);
137 mPortWrap
= (TextInputLayoutHelper
) findViewById(R
.id
.port_wrap
);
138 mBlockIPv4
= (CheckBox
)findViewById(R
.id
.split_tunneling_v4
);
139 mBlockIPv6
= (CheckBox
)findViewById(R
.id
.split_tunneling_v6
);
141 mGateway
.addTextChangedListener(new TextWatcher() {
143 public void beforeTextChanged(CharSequence s
, int start
, int count
, int after
) {}
146 public void onTextChanged(CharSequence s
, int start
, int before
, int count
) {}
149 public void afterTextChanged(Editable s
)
151 if (TextUtils
.isEmpty(mGateway
.getText()))
153 mNameWrap
.setHelperText(getString(R
.string
.profile_name_hint
));
157 mNameWrap
.setHelperText(String
.format(getString(R
.string
.profile_name_hint_gateway
), mGateway
.getText()));
162 mSelectVpnType
.setOnItemSelectedListener(new OnItemSelectedListener() {
164 public void onItemSelected(AdapterView
<?
> parent
, View view
, int position
, long id
)
166 mVpnType
= VpnType
.values()[position
];
167 updateCredentialView();
171 public void onNothingSelected(AdapterView
<?
> parent
)
172 { /* should not happen */
173 mVpnType
= VpnType
.IKEV2_EAP
;
174 updateCredentialView();
178 ((TextView
)mTncNotice
.findViewById(android
.R
.id
.text1
)).setText(R
.string
.tnc_notice_title
);
179 ((TextView
)mTncNotice
.findViewById(android
.R
.id
.text2
)).setText(R
.string
.tnc_notice_subtitle
);
180 mTncNotice
.setOnClickListener(new OnClickListener() {
182 public void onClick(View v
)
184 new TncNoticeDialog().show(VpnProfileDetailActivity
.this.getSupportFragmentManager(), "TncNotice");
188 mSelectUserCert
.setOnClickListener(new SelectUserCertOnClickListener());
190 mCheckAuto
.setOnCheckedChangeListener(new OnCheckedChangeListener() {
192 public void onCheckedChanged(CompoundButton buttonView
, boolean isChecked
)
194 updateCertificateSelector();
198 mSelectCert
.setOnClickListener(new OnClickListener() {
200 public void onClick(View v
)
202 Intent intent
= new Intent(VpnProfileDetailActivity
.this, TrustedCertificatesActivity
.class);
203 intent
.setAction(TrustedCertificatesActivity
.SELECT_CERTIFICATE
);
204 startActivityForResult(intent
, SELECT_TRUSTED_CERTIFICATE
);
208 mShowAdvanced
.setOnCheckedChangeListener(new OnCheckedChangeListener() {
210 public void onCheckedChanged(CompoundButton buttonView
, boolean isChecked
)
212 updateAdvancedSettings();
216 mId
= savedInstanceState
== null ? null
: savedInstanceState
.getLong(VpnProfileDataSource
.KEY_ID
);
219 Bundle extras
= getIntent().getExtras();
220 mId
= extras
== null ? null
: extras
.getLong(VpnProfileDataSource
.KEY_ID
);
223 loadProfileData(savedInstanceState
);
225 updateCredentialView();
226 updateCertificateSelector();
227 updateAdvancedSettings();
231 protected void onDestroy()
238 protected void onSaveInstanceState(Bundle outState
)
240 super.onSaveInstanceState(outState
);
243 outState
.putLong(VpnProfileDataSource
.KEY_ID
, mId
);
245 if (mUserCertEntry
!= null
)
247 outState
.putString(VpnProfileDataSource
.KEY_USER_CERTIFICATE
, mUserCertEntry
.getAlias());
249 if (mCertEntry
!= null
)
251 outState
.putString(VpnProfileDataSource
.KEY_CERTIFICATE
, mCertEntry
.getAlias());
256 public boolean onCreateOptionsMenu(Menu menu
)
258 MenuInflater inflater
= getMenuInflater();
259 inflater
.inflate(R
.menu
.profile_edit
, menu
);
264 public boolean onOptionsItemSelected(MenuItem item
)
266 switch (item
.getItemId())
268 case android
.R
.id
.home
:
269 case R
.id
.menu_cancel
:
272 case R
.id
.menu_accept
:
276 return super.onOptionsItemSelected(item
);
281 protected void onActivityResult(int requestCode
, int resultCode
, Intent data
)
285 case SELECT_TRUSTED_CERTIFICATE
:
286 if (resultCode
== RESULT_OK
)
288 String alias
= data
.getStringExtra(VpnProfileDataSource
.KEY_CERTIFICATE
);
289 X509Certificate certificate
= TrustedCertificateManager
.getInstance().getCACertificateFromAlias(alias
);
290 mCertEntry
= certificate
== null ? null
: new TrustedCertificateEntry(alias
, certificate
);
291 updateCertificateSelector();
295 super.onActivityResult(requestCode
, resultCode
, data
);
300 * Update the UI to enter credentials depending on the type of VPN currently selected
302 private void updateCredentialView()
304 mUsernamePassword
.setVisibility(mVpnType
.has(VpnTypeFeature
.USER_PASS
) ? View
.VISIBLE
: View
.GONE
);
305 mUserCertificate
.setVisibility(mVpnType
.has(VpnTypeFeature
.CERTIFICATE
) ? View
.VISIBLE
: View
.GONE
);
306 mTncNotice
.setVisibility(mVpnType
.has(VpnTypeFeature
.BYOD
) ? View
.VISIBLE
: View
.GONE
);
308 if (mVpnType
.has(VpnTypeFeature
.CERTIFICATE
))
310 if (mUserCertLoading
!= null
)
312 ((TextView
)mSelectUserCert
.findViewById(android
.R
.id
.text1
)).setText(mUserCertLoading
);
313 ((TextView
)mSelectUserCert
.findViewById(android
.R
.id
.text2
)).setText(R
.string
.loading
);
315 else if (mUserCertEntry
!= null
)
316 { /* clear any errors and set the new data */
317 ((TextView
)mSelectUserCert
.findViewById(android
.R
.id
.text1
)).setError(null
);
318 ((TextView
)mSelectUserCert
.findViewById(android
.R
.id
.text1
)).setText(mUserCertEntry
.getAlias());
319 ((TextView
)mSelectUserCert
.findViewById(android
.R
.id
.text2
)).setText(mUserCertEntry
.getCertificate().getSubjectDN().toString());
323 ((TextView
)mSelectUserCert
.findViewById(android
.R
.id
.text1
)).setText(R
.string
.profile_user_select_certificate_label
);
324 ((TextView
)mSelectUserCert
.findViewById(android
.R
.id
.text2
)).setText(R
.string
.profile_user_select_certificate
);
330 * Show an alert in case the previously selected certificate is not found anymore
331 * or the user did not select a certificate in the spinner.
333 private void showCertificateAlert()
335 AlertDialog
.Builder adb
= new AlertDialog
.Builder(VpnProfileDetailActivity
.this);
336 adb
.setTitle(R
.string
.alert_text_nocertfound_title
);
337 adb
.setMessage(R
.string
.alert_text_nocertfound
);
338 adb
.setPositiveButton(android
.R
.string
.ok
, new DialogInterface
.OnClickListener() {
340 public void onClick(DialogInterface dialog
, int id
)
349 * Update the CA certificate selection UI depending on whether the
350 * certificate should be automatically selected or not.
352 private void updateCertificateSelector()
354 if (!mCheckAuto
.isChecked())
356 mSelectCert
.setEnabled(true
);
357 mSelectCert
.setVisibility(View
.VISIBLE
);
359 if (mCertEntry
!= null
)
361 ((TextView
)mSelectCert
.findViewById(android
.R
.id
.text1
)).setText(mCertEntry
.getSubjectPrimary());
362 ((TextView
)mSelectCert
.findViewById(android
.R
.id
.text2
)).setText(mCertEntry
.getSubjectSecondary());
366 ((TextView
)mSelectCert
.findViewById(android
.R
.id
.text1
)).setText(R
.string
.profile_ca_select_certificate_label
);
367 ((TextView
)mSelectCert
.findViewById(android
.R
.id
.text2
)).setText(R
.string
.profile_ca_select_certificate
);
372 mSelectCert
.setEnabled(false
);
373 mSelectCert
.setVisibility(View
.GONE
);
378 * Update the advanced settings UI depending on whether any advanced
379 * settings have already been made.
381 private void updateAdvancedSettings()
383 boolean show
= mShowAdvanced
.isChecked();
384 if (!show
&& mProfile
!= null
)
386 Integer st
= mProfile
.getSplitTunneling();
387 show
= mProfile
.getMTU() != null
|| mProfile
.getPort() != null
|| (st
!= null
&& st
!= 0);
389 mShowAdvanced
.setVisibility(!show ? View
.VISIBLE
: View
.GONE
);
390 mAdvancedSettings
.setVisibility(show ? View
.VISIBLE
: View
.GONE
);
394 * Save or update the profile depending on whether we actually have a
395 * profile object or not (this was created in updateProfileData)
397 private void saveProfile()
401 if (mProfile
!= null
)
404 mDataSource
.updateVpnProfile(mProfile
);
408 mProfile
= new VpnProfile();
410 mDataSource
.insertProfile(mProfile
);
412 setResult(RESULT_OK
, new Intent().putExtra(VpnProfileDataSource
.KEY_ID
, mProfile
.getId()));
418 * Verify the user input and display error messages.
419 * @return true if the input is valid
421 private boolean verifyInput()
423 boolean valid
= true
;
424 if (mGateway
.getText().toString().trim().isEmpty())
426 mGatewayWrap
.setError(getString(R
.string
.alert_text_no_input_gateway
));
429 if (mVpnType
.has(VpnTypeFeature
.USER_PASS
))
431 if (mUsername
.getText().toString().trim().isEmpty())
433 mUsernameWrap
.setError(getString(R
.string
.alert_text_no_input_username
));
437 if (mVpnType
.has(VpnTypeFeature
.CERTIFICATE
) && mUserCertEntry
== null
)
438 { /* let's show an error icon */
439 ((TextView
)mSelectUserCert
.findViewById(android
.R
.id
.text1
)).setError("");
442 if (!mCheckAuto
.isChecked() && mCertEntry
== null
)
444 showCertificateAlert();
447 Integer mtu
= getInteger(mMTU
);
448 if (mtu
!= null
&& (mtu
< MTU_MIN
|| mtu
> MTU_MAX
))
450 mMTUWrap
.setError(String
.format(getString(R
.string
.alert_text_out_of_range
), MTU_MIN
, MTU_MAX
));
453 Integer port
= getInteger(mPort
);
454 if (port
!= null
&& (port
< 1 || port
> 65535))
456 mPortWrap
.setError(String
.format(getString(R
.string
.alert_text_out_of_range
), 1, 65535));
463 * Update the profile object with the data entered by the user
465 private void updateProfileData()
467 /* the name is optional, we default to the gateway if none is given */
468 String name
= mName
.getText().toString().trim();
469 String gateway
= mGateway
.getText().toString().trim();
470 mProfile
.setName(name
.isEmpty() ? gateway
: name
);
471 mProfile
.setGateway(gateway
);
472 mProfile
.setVpnType(mVpnType
);
473 if (mVpnType
.has(VpnTypeFeature
.USER_PASS
))
475 mProfile
.setUsername(mUsername
.getText().toString().trim());
476 String password
= mPassword
.getText().toString().trim();
477 password
= password
.isEmpty() ? null
: password
;
478 mProfile
.setPassword(password
);
480 if (mVpnType
.has(VpnTypeFeature
.CERTIFICATE
))
482 mProfile
.setUserCertificateAlias(mUserCertEntry
.getAlias());
484 String certAlias
= mCheckAuto
.isChecked() ? null
: mCertEntry
.getAlias();
485 mProfile
.setCertificateAlias(certAlias
);
486 mProfile
.setMTU(getInteger(mMTU
));
487 mProfile
.setPort(getInteger(mPort
));
489 st
|= mBlockIPv4
.isChecked() ? VpnProfile
.SPLIT_TUNNELING_BLOCK_IPV4
: 0;
490 st
|= mBlockIPv6
.isChecked() ? VpnProfile
.SPLIT_TUNNELING_BLOCK_IPV6
: 0;
491 mProfile
.setSplitTunneling(st
== 0 ? null
: st
);
495 * Load an existing profile if we got an ID
497 * @param savedInstanceState previously saved state
499 private void loadProfileData(Bundle savedInstanceState
)
501 String useralias
= null
, alias
= null
;
503 getSupportActionBar().setTitle(R
.string
.add_profile
);
504 if (mId
!= null
&& mId
!= 0)
506 mProfile
= mDataSource
.getVpnProfile(mId
);
507 if (mProfile
!= null
)
509 mName
.setText(mProfile
.getName());
510 mGateway
.setText(mProfile
.getGateway());
511 mVpnType
= mProfile
.getVpnType();
512 mUsername
.setText(mProfile
.getUsername());
513 mPassword
.setText(mProfile
.getPassword());
514 mMTU
.setText(mProfile
.getMTU() != null ? mProfile
.getMTU().toString() : null
);
515 mPort
.setText(mProfile
.getPort() != null ? mProfile
.getPort().toString() : null
);
516 mBlockIPv4
.setChecked(mProfile
.getSplitTunneling() != null ?
(mProfile
.getSplitTunneling() & VpnProfile
.SPLIT_TUNNELING_BLOCK_IPV4
) != 0 : false
);
517 mBlockIPv6
.setChecked(mProfile
.getSplitTunneling() != null ?
(mProfile
.getSplitTunneling() & VpnProfile
.SPLIT_TUNNELING_BLOCK_IPV6
) != 0 : false
);
518 useralias
= mProfile
.getUserCertificateAlias();
519 alias
= mProfile
.getCertificateAlias();
520 getSupportActionBar().setTitle(mProfile
.getName());
524 Log
.e(VpnProfileDetailActivity
.class.getSimpleName(),
525 "VPN profile with id " + mId
+ " not found");
530 mSelectVpnType
.setSelection(mVpnType
.ordinal());
532 /* check if the user selected a user certificate previously */
533 useralias
= savedInstanceState
== null ? useralias
: savedInstanceState
.getString(VpnProfileDataSource
.KEY_USER_CERTIFICATE
);
534 if (useralias
!= null
)
536 UserCertificateLoader loader
= new UserCertificateLoader(this, useralias
);
537 mUserCertLoading
= useralias
;
541 /* check if the user selected a CA certificate previously */
542 alias
= savedInstanceState
== null ? alias
: savedInstanceState
.getString(VpnProfileDataSource
.KEY_CERTIFICATE
);
543 mCheckAuto
.setChecked(alias
== null
);
546 X509Certificate certificate
= TrustedCertificateManager
.getInstance().getCACertificateFromAlias(alias
);
547 if (certificate
!= null
)
549 mCertEntry
= new TrustedCertificateEntry(alias
, certificate
);
552 { /* previously selected certificate is not here anymore */
553 showCertificateAlert();
560 * Get the integer value in the given text box or null if empty
562 * @param view text box (numeric entry assumed)
564 private Integer
getInteger(EditText view
)
566 String value
= view
.getText().toString().trim();
567 return value
.isEmpty() ? null
: Integer
.valueOf(value
);
570 private class SelectUserCertOnClickListener
implements OnClickListener
, KeyChainAliasCallback
573 public void onClick(View v
)
575 String useralias
= mUserCertEntry
!= null ? mUserCertEntry
.getAlias() : null
;
576 KeyChain
.choosePrivateKeyAlias(VpnProfileDetailActivity
.this, this, new String
[] { "RSA" }, null
, null
, -1, useralias
);
580 public void alias(final String alias
)
583 { /* otherwise the dialog was canceled, the request denied */
586 final X509Certificate
[] chain
= KeyChain
.getCertificateChain(VpnProfileDetailActivity
.this, alias
);
587 /* alias() is not called from our main thread */
588 runOnUiThread(new Runnable() {
592 if (chain
!= null
&& chain
.length
> 0)
594 mUserCertEntry
= new TrustedCertificateEntry(alias
, chain
[0]);
596 updateCredentialView();
600 catch (KeyChainException e
)
604 catch (InterruptedException e
)
613 * Load the selected user certificate asynchronously. This cannot be done
614 * from the main thread as getCertificateChain() calls back to our main
615 * thread to bind to the KeyChain service resulting in a deadlock.
617 private class UserCertificateLoader
extends AsyncTask
<Void
, Void
, X509Certificate
>
619 private final Context mContext
;
620 private final String mAlias
;
622 public UserCertificateLoader(Context context
, String alias
)
629 protected X509Certificate
doInBackground(Void
... params
)
631 X509Certificate
[] chain
= null
;
634 chain
= KeyChain
.getCertificateChain(mContext
, mAlias
);
636 catch (KeyChainException e
)
640 catch (InterruptedException e
)
644 if (chain
!= null
&& chain
.length
> 0)
652 protected void onPostExecute(X509Certificate result
)
656 mUserCertEntry
= new TrustedCertificateEntry(mAlias
, result
);
659 { /* previously selected certificate is not here anymore */
660 ((TextView
)mSelectUserCert
.findViewById(android
.R
.id
.text1
)).setError("");
661 mUserCertEntry
= null
;
663 mUserCertLoading
= null
;
664 updateCredentialView();
669 * Dialog with notification message if EAP-TNC is used.
671 public static class TncNoticeDialog
extends AppCompatDialogFragment
674 public Dialog
onCreateDialog(Bundle savedInstanceState
)
676 return new AlertDialog
.Builder(getActivity())
677 .setTitle(R
.string
.tnc_notice_title
)
678 .setMessage(Html
.fromHtml(getString(R
.string
.tnc_notice_details
)))
679 .setPositiveButton(android
.R
.string
.ok
, new DialogInterface
.OnClickListener() {
681 public void onClick(DialogInterface dialog
, int id
)