]> git.ipfire.org Git - thirdparty/strongswan.git/blob - src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java
1d8b677eed0473ff67154065f8a30308cb4d3dcf
[thirdparty/strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / ui / VpnProfileImportActivity.java
1 /*
2 * Copyright (C) 2016-2018 Tobias Brunner
3 * HSR Hochschule fuer Technik Rapperswil
4 *
5 * This program is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License as published by the
7 * Free Software Foundation; either version 2 of the License, or (at your
8 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
9 *
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13 * for more details.
14 */
15
16 package org.strongswan.android.ui;
17
18 import android.app.Activity;
19 import android.app.LoaderManager;
20 import android.content.ActivityNotFoundException;
21 import android.content.AsyncTaskLoader;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.Loader;
26 import android.net.Uri;
27 import android.os.Build;
28 import android.os.Bundle;
29 import android.security.KeyChain;
30 import android.security.KeyChainAliasCallback;
31 import android.security.KeyChainException;
32 import android.support.v4.content.LocalBroadcastManager;
33 import android.support.v7.app.AppCompatActivity;
34 import android.text.TextUtils;
35 import android.util.Base64;
36 import android.view.Menu;
37 import android.view.MenuInflater;
38 import android.view.MenuItem;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.widget.Button;
42 import android.widget.EditText;
43 import android.widget.RelativeLayout;
44 import android.widget.TextView;
45 import android.widget.Toast;
46
47 import org.json.JSONArray;
48 import org.json.JSONException;
49 import org.json.JSONObject;
50 import org.strongswan.android.R;
51 import org.strongswan.android.data.VpnProfile;
52 import org.strongswan.android.data.VpnProfile.SelectedAppsHandling;
53 import org.strongswan.android.data.VpnProfileDataSource;
54 import org.strongswan.android.data.VpnType;
55 import org.strongswan.android.data.VpnType.VpnTypeFeature;
56 import org.strongswan.android.logic.TrustedCertificateManager;
57 import org.strongswan.android.security.TrustedCertificateEntry;
58 import org.strongswan.android.ui.widget.TextInputLayoutHelper;
59 import org.strongswan.android.utils.Constants;
60 import org.strongswan.android.utils.IPRangeSet;
61 import org.strongswan.android.utils.Utils;
62
63 import java.io.ByteArrayInputStream;
64 import java.io.ByteArrayOutputStream;
65 import java.io.FileNotFoundException;
66 import java.io.IOException;
67 import java.io.InputStream;
68 import java.net.URL;
69 import java.net.UnknownHostException;
70 import java.security.KeyStore;
71 import java.security.KeyStoreException;
72 import java.security.NoSuchAlgorithmException;
73 import java.security.cert.CertificateException;
74 import java.security.cert.CertificateFactory;
75 import java.security.cert.X509Certificate;
76 import java.util.ArrayList;
77 import java.util.UUID;
78
79 import javax.net.ssl.SSLHandshakeException;
80
81 public class VpnProfileImportActivity extends AppCompatActivity
82 {
83 private static final String PKCS12_INSTALLED = "PKCS12_INSTALLED";
84 private static final String PROFILE_URI = "PROFILE_URI";
85 private static final int INSTALL_PKCS12 = 0;
86 private static final int OPEN_DOCUMENT = 1;
87 private static final int PROFILE_LOADER = 0;
88 private static final int USER_CERT_LOADER = 1;
89
90 private VpnProfileDataSource mDataSource;
91 private ParsedVpnProfile mProfile;
92 private VpnProfile mExisting;
93 private TrustedCertificateEntry mCertEntry;
94 private TrustedCertificateEntry mUserCertEntry;
95 private String mUserCertLoading;
96 private boolean mHideImport;
97 private android.support.v4.widget.ContentLoadingProgressBar mProgressBar;
98 private TextView mExistsWarning;
99 private ViewGroup mBasicDataGroup;
100 private TextView mName;
101 private TextView mGateway;
102 private TextView mSelectVpnType;
103 private ViewGroup mUsernamePassword;
104 private EditText mUsername;
105 private TextInputLayoutHelper mUsernameWrap;
106 private EditText mPassword;
107 private ViewGroup mUserCertificate;
108 private RelativeLayout mSelectUserCert;
109 private Button mImportUserCert;
110 private ViewGroup mRemoteCertificate;
111 private RelativeLayout mRemoteCert;
112
113 private LoaderManager.LoaderCallbacks<ProfileLoadResult> mProfileLoaderCallbacks = new LoaderManager.LoaderCallbacks<ProfileLoadResult>()
114 {
115 @Override
116 public Loader<ProfileLoadResult> onCreateLoader(int id, Bundle args)
117 {
118 return new ProfileLoader(VpnProfileImportActivity.this, (Uri)args.getParcelable(PROFILE_URI));
119 }
120
121 @Override
122 public void onLoadFinished(Loader<ProfileLoadResult> loader, ProfileLoadResult data)
123 {
124 handleProfile(data);
125 }
126
127 @Override
128 public void onLoaderReset(Loader<ProfileLoadResult> loader)
129 {
130
131 }
132 };
133
134 private LoaderManager.LoaderCallbacks<TrustedCertificateEntry> mUserCertificateLoaderCallbacks = new LoaderManager.LoaderCallbacks<TrustedCertificateEntry>()
135 {
136 @Override
137 public Loader<TrustedCertificateEntry> onCreateLoader(int id, Bundle args)
138 {
139 return new UserCertificateLoader(VpnProfileImportActivity.this, mUserCertLoading);
140 }
141
142 @Override
143 public void onLoadFinished(Loader<TrustedCertificateEntry> loader, TrustedCertificateEntry data)
144 {
145 handleUserCertificate(data);
146 }
147
148 @Override
149 public void onLoaderReset(Loader<TrustedCertificateEntry> loader)
150 {
151
152 }
153 };
154
155 @Override
156 public void onCreate(Bundle savedInstanceState)
157 {
158 super.onCreate(savedInstanceState);
159
160 getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
161 getSupportActionBar().setDisplayHomeAsUpEnabled(true);
162
163 mDataSource = new VpnProfileDataSource(this);
164 mDataSource.open();
165
166 setContentView(R.layout.profile_import_view);
167
168 mProgressBar = findViewById(R.id.progress_bar);
169 mExistsWarning = (TextView)findViewById(R.id.exists_warning);
170 mBasicDataGroup = (ViewGroup)findViewById(R.id.basic_data_group);
171 mName = (TextView)findViewById(R.id.name);
172 mGateway = (TextView)findViewById(R.id.gateway);
173 mSelectVpnType = (TextView)findViewById(R.id.vpn_type);
174
175 mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
176 mUsername = (EditText)findViewById(R.id.username);
177 mUsernameWrap = (TextInputLayoutHelper) findViewById(R.id.username_wrap);
178 mPassword = (EditText)findViewById(R.id.password);
179
180 mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group);
181 mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate);
182 mImportUserCert = (Button)findViewById(R.id.import_user_certificate);
183
184 mRemoteCertificate = (ViewGroup)findViewById(R.id.remote_certificate_group);
185 mRemoteCert = (RelativeLayout)findViewById(R.id.remote_certificate);
186
187 mExistsWarning.setVisibility(View.GONE);
188 mBasicDataGroup.setVisibility(View.GONE);
189 mUsernamePassword.setVisibility(View.GONE);
190 mUserCertificate.setVisibility(View.GONE);
191 mRemoteCertificate.setVisibility(View.GONE);
192
193 mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
194 mImportUserCert.setOnClickListener(new View.OnClickListener() {
195 @Override
196 public void onClick(View v)
197 {
198 Intent intent = KeyChain.createInstallIntent();
199 intent.putExtra(KeyChain.EXTRA_NAME, getString(R.string.profile_cert_alias, mProfile.getName()));
200 intent.putExtra(KeyChain.EXTRA_PKCS12, mProfile.PKCS12);
201 startActivityForResult(intent, INSTALL_PKCS12);
202 }
203 });
204
205 Intent intent = getIntent();
206 String action = intent.getAction();
207 if (Intent.ACTION_VIEW.equals(action))
208 {
209 loadProfile(getIntent().getData());
210 }
211 else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
212 {
213 Intent openIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
214 openIntent.setType("*/*");
215 try
216 {
217 startActivityForResult(openIntent, OPEN_DOCUMENT);
218 }
219 catch (ActivityNotFoundException e)
220 { /* some devices are unable to browse for files */
221 finish();
222 return;
223 }
224 }
225
226 if (savedInstanceState != null)
227 {
228 mUserCertLoading = savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
229 if (mUserCertLoading != null)
230 {
231 getLoaderManager().initLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
232 }
233 mImportUserCert.setEnabled(!savedInstanceState.getBoolean(PKCS12_INSTALLED));
234 }
235 }
236
237 @Override
238 protected void onDestroy()
239 {
240 super.onDestroy();
241 mDataSource.close();
242 }
243
244 @Override
245 protected void onSaveInstanceState(Bundle outState)
246 {
247 super.onSaveInstanceState(outState);
248 if (mUserCertEntry != null)
249 {
250 outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
251 }
252 outState.putBoolean(PKCS12_INSTALLED, !mImportUserCert.isEnabled());
253 }
254
255 @Override
256 public boolean onCreateOptionsMenu(Menu menu)
257 {
258 MenuInflater inflater = getMenuInflater();
259 inflater.inflate(R.menu.profile_import, menu);
260 if (mHideImport)
261 {
262 MenuItem item = menu.findItem(R.id.menu_accept);
263 item.setVisible(false);
264 }
265 return true;
266 }
267
268 @Override
269 public boolean onOptionsItemSelected(MenuItem item)
270 {
271 switch (item.getItemId())
272 {
273 case android.R.id.home:
274 finish();
275 return true;
276 case R.id.menu_accept:
277 saveProfile();
278 return true;
279 default:
280 return super.onOptionsItemSelected(item);
281 }
282 }
283
284 @Override
285 protected void onActivityResult(int requestCode, int resultCode, Intent data)
286 {
287 super.onActivityResult(requestCode, resultCode, data);
288 switch (requestCode)
289 {
290 case INSTALL_PKCS12:
291 if (resultCode == Activity.RESULT_OK)
292 { /* no need to import twice */
293 mImportUserCert.setEnabled(false);
294 mSelectUserCert.performClick();
295 }
296 break;
297 case OPEN_DOCUMENT:
298 if (resultCode == Activity.RESULT_OK && data != null)
299 {
300 loadProfile(data.getData());
301 return;
302 }
303 finish();
304 break;
305 }
306 }
307
308 private void loadProfile(Uri uri)
309 {
310 mProgressBar.show();
311
312 Bundle args = new Bundle();
313 args.putParcelable(PROFILE_URI, uri);
314 getLoaderManager().initLoader(PROFILE_LOADER, args, mProfileLoaderCallbacks);
315 }
316
317 public void handleProfile(ProfileLoadResult data)
318 {
319 mProgressBar.hide();
320
321 mProfile = null;
322 if (data != null && data.ThrownException == null)
323 {
324 try
325 {
326 JSONObject obj = new JSONObject(data.Profile);
327 mProfile = parseProfile(obj);
328 }
329 catch (JSONException e)
330 {
331 mExistsWarning.setVisibility(View.VISIBLE);
332 mExistsWarning.setText(e.getLocalizedMessage());
333 mHideImport = true;
334 invalidateOptionsMenu();
335 return;
336 }
337 }
338 if (mProfile == null)
339 {
340 String error = null;
341 if (data.ThrownException != null)
342 {
343 try
344 {
345 throw data.ThrownException;
346 }
347 catch (FileNotFoundException e)
348 {
349 error = getString(R.string.profile_import_failed_not_found);
350 }
351 catch (UnknownHostException e)
352 {
353 error = getString(R.string.profile_import_failed_host);
354 }
355 catch (SSLHandshakeException e)
356 {
357 error = getString(R.string.profile_import_failed_tls);
358 }
359 catch (Exception e)
360 {
361 e.printStackTrace();
362 }
363 }
364 if (error != null)
365 {
366 Toast.makeText(this, getString(R.string.profile_import_failed_detail, error), Toast.LENGTH_LONG).show();
367 }
368 else
369 {
370 Toast.makeText(this, R.string.profile_import_failed, Toast.LENGTH_LONG).show();
371 }
372 finish();
373 return;
374 }
375 mExisting = mDataSource.getVpnProfile(mProfile.getUUID());
376 mExistsWarning.setVisibility(mExisting != null ? View.VISIBLE : View.GONE);
377
378 mBasicDataGroup.setVisibility(View.VISIBLE);
379 mName.setText(mProfile.getName());
380 mGateway.setText(mProfile.getGateway());
381 mSelectVpnType.setText(getResources().getStringArray(R.array.vpn_types)[mProfile.getVpnType().ordinal()]);
382
383 mUsernamePassword.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE);
384 if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
385 {
386 mUsername.setText(mProfile.getUsername());
387 if (mProfile.getUsername() != null && !mProfile.getUsername().isEmpty())
388 {
389 mUsername.setEnabled(false);
390 }
391 }
392
393 mUserCertificate.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE);
394 mRemoteCertificate.setVisibility(mProfile.Certificate != null ? View.VISIBLE : View.GONE);
395 mImportUserCert.setVisibility(mProfile.PKCS12 != null ? View.VISIBLE : View.GONE);
396
397 if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE))
398 { /* try to load an existing certificate with the default name */
399 if (mUserCertLoading == null)
400 {
401 mUserCertLoading = getString(R.string.profile_cert_alias, mProfile.getName());
402 getLoaderManager().initLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
403 }
404 updateUserCertView();
405 }
406
407 if (mProfile.Certificate != null)
408 {
409 try
410 {
411 CertificateFactory factory = CertificateFactory.getInstance("X.509");
412 X509Certificate certificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(mProfile.Certificate));
413 KeyStore store = KeyStore.getInstance("LocalCertificateStore");
414 store.load(null, null);
415 String alias = store.getCertificateAlias(certificate);
416 mCertEntry = new TrustedCertificateEntry(alias, certificate);
417 ((TextView)mRemoteCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
418 ((TextView)mRemoteCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
419 }
420 catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e)
421 {
422 e.printStackTrace();
423 mRemoteCertificate.setVisibility(View.GONE);
424 }
425 }
426 }
427
428 private void handleUserCertificate(TrustedCertificateEntry data)
429 {
430 mUserCertEntry = data;
431 mUserCertLoading = null;
432 updateUserCertView();
433 }
434
435 private void updateUserCertView()
436 {
437 if (mUserCertLoading != null)
438 {
439 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
440 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
441 }
442 else if (mUserCertEntry != null)
443 { /* clear any errors and set the new data */
444 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
445 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
446 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
447 }
448 else
449 {
450 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label);
451 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate);
452 }
453 }
454
455 private ParsedVpnProfile parseProfile(JSONObject obj) throws JSONException
456 {
457 UUID uuid;
458 try
459 {
460 uuid = UUID.fromString(obj.getString("uuid"));
461 }
462 catch (IllegalArgumentException e)
463 {
464 e.printStackTrace();
465 return null;
466 }
467 ParsedVpnProfile profile = new ParsedVpnProfile();
468 Integer flags = 0;
469
470 profile.setUUID(uuid);
471 profile.setName(obj.getString("name"));
472 VpnType type = VpnType.fromIdentifier(obj.getString("type"));
473 profile.setVpnType(type);
474
475 JSONObject remote = obj.getJSONObject("remote");
476 profile.setGateway(remote.getString("addr"));
477 profile.setPort(getInteger(remote, "port", 1, 65535));
478 profile.setRemoteId(remote.optString("id", null));
479 profile.Certificate = decodeBase64(remote.optString("cert", null));
480
481 if (!remote.optBoolean("certreq", true))
482 {
483 flags |= VpnProfile.FLAGS_SUPPRESS_CERT_REQS;
484 }
485
486 JSONObject revocation = remote.optJSONObject("revocation");
487 if (revocation != null)
488 {
489 if (!revocation.optBoolean("crl", true))
490 {
491 flags |= VpnProfile.FLAGS_DISABLE_CRL;
492 }
493 if (!revocation.optBoolean("ocsp", true))
494 {
495 flags |= VpnProfile.FLAGS_DISABLE_OCSP;
496 }
497 if (revocation.optBoolean("strict", false))
498 {
499 flags |= VpnProfile.FLAGS_STRICT_REVOCATION;
500 }
501 }
502
503 JSONObject local = obj.optJSONObject("local");
504 if (local != null)
505 {
506 if (type.has(VpnTypeFeature.USER_PASS))
507 {
508 profile.setUsername(local.optString("eap_id", null));
509 }
510
511 if (type.has(VpnTypeFeature.CERTIFICATE))
512 {
513 profile.setLocalId(local.optString("id", null));
514 profile.PKCS12 = decodeBase64(local.optString("p12", null));
515
516 if (local.optBoolean("rsa-pss", false))
517 {
518 flags |= VpnProfile.FLAGS_RSA_PSS;
519 }
520 }
521 }
522
523 profile.setIkeProposal(getProposal(obj, "ike-proposal", true));
524 profile.setEspProposal(getProposal(obj, "esp-proposal", false));
525 profile.setMTU(getInteger(obj, "mtu", Constants.MTU_MIN, Constants.MTU_MAX));
526 profile.setNATKeepAlive(getInteger(obj, "nat-keepalive", Constants.NAT_KEEPALIVE_MIN, Constants.NAT_KEEPALIVE_MAX));
527 JSONObject split = obj.optJSONObject("split-tunneling");
528 if (split != null)
529 {
530 String included = getSubnets(split, "subnets");
531 profile.setIncludedSubnets(included != null ? included : null);
532 String excluded = getSubnets(split, "excluded");
533 profile.setExcludedSubnets(excluded != null ? excluded : null);
534 int st = 0;
535 st |= split.optBoolean("block-ipv4") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
536 st |= split.optBoolean("block-ipv6") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
537 profile.setSplitTunneling(st == 0 ? null : st);
538 }
539 /* only one of these can be set, prefer specific apps */
540 String selectedApps = getApps(obj.optJSONArray("apps"));
541 String excludedApps = getApps(obj.optJSONArray("excluded-apps"));
542 if (!TextUtils.isEmpty(selectedApps))
543 {
544 profile.setSelectedApps(selectedApps);
545 profile.setSelectedAppsHandling(SelectedAppsHandling.SELECTED_APPS_ONLY);
546 }
547 else if (!TextUtils.isEmpty(excludedApps))
548 {
549 profile.setSelectedApps(excludedApps);
550 profile.setSelectedAppsHandling(SelectedAppsHandling.SELECTED_APPS_EXCLUDE);
551 }
552 profile.setFlags(flags);
553 return profile;
554 }
555
556 private Integer getInteger(JSONObject obj, String key, int min, int max)
557 {
558 Integer res = obj.optInt(key);
559 return res < min || res > max ? null : res;
560 }
561
562 private String getProposal(JSONObject obj, String key, boolean ike) throws JSONException
563 {
564 String value = obj.optString(key, null);
565 if (!TextUtils.isEmpty(value))
566 {
567 if (!Utils.isProposalValid(ike, value))
568 {
569 throw new JSONException(getString(R.string.profile_import_failed_value, key));
570 }
571 }
572 return value;
573 }
574
575 private String getSubnets(JSONObject split, String key) throws JSONException
576 {
577 ArrayList<String> subnets = new ArrayList<>();
578 JSONArray arr = split.optJSONArray(key);
579 if (arr != null)
580 {
581 for (int i = 0; i < arr.length(); i++)
582 { /* replace all spaces, e.g. in "192.168.1.1 - 192.168.1.10" */
583 subnets.add(arr.getString(i).replace(" ", ""));
584 }
585 }
586 else
587 {
588 String value = split.optString(key, null);
589 if (!TextUtils.isEmpty(value))
590 {
591 subnets.add(value);
592 }
593 }
594 if (subnets.size() > 0)
595 {
596 String joined = TextUtils.join(" ", subnets);
597 IPRangeSet ranges = IPRangeSet.fromString(joined);
598 if (ranges == null)
599 {
600 throw new JSONException(getString(R.string.profile_import_failed_value,
601 "split-tunneling." + key));
602 }
603 return ranges.toString();
604 }
605 return null;
606 }
607
608 private String getApps(JSONArray arr) throws JSONException
609 {
610 ArrayList<String> apps = new ArrayList<>();
611 if (arr != null)
612 {
613 for (int i = 0; i < arr.length(); i++)
614 {
615 apps.add(arr.getString(i));
616 }
617 }
618 return TextUtils.join(" ", apps);
619 }
620
621 /**
622 * Save or update the profile depending on whether we actually have a
623 * profile object or not (this was created in updateProfileData)
624 */
625 private void saveProfile()
626 {
627 if (verifyInput())
628 {
629 updateProfileData();
630 if (mExisting != null)
631 {
632 mProfile.setId(mExisting.getId());
633 mDataSource.updateVpnProfile(mProfile);
634 }
635 else
636 {
637 mDataSource.insertProfile(mProfile);
638 }
639 if (mCertEntry != null)
640 {
641 try
642 { /* store the CA/server certificate */
643 KeyStore store = KeyStore.getInstance("LocalCertificateStore");
644 store.load(null, null);
645 store.setCertificateEntry(null, mCertEntry.getCertificate());
646 TrustedCertificateManager.getInstance().reset();
647 }
648 catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e)
649 {
650 e.printStackTrace();
651 }
652 }
653 Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED);
654 intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId());
655 LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
656
657 intent = new Intent(this, MainActivity.class);
658 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
659 startActivity(intent);
660
661 setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
662 finish();
663 }
664 }
665
666 /**
667 * Verify the user input and display error messages.
668 * @return true if the input is valid
669 */
670 private boolean verifyInput()
671 {
672 boolean valid = true;
673 if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
674 {
675 if (mUsername.getText().toString().trim().isEmpty())
676 {
677 mUsernameWrap.setError(getString(R.string.alert_text_no_input_username));
678 valid = false;
679 }
680 }
681 if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null)
682 { /* let's show an error icon */
683 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
684 valid = false;
685 }
686 return valid;
687 }
688
689 /**
690 * Update the profile object with the data entered by the user
691 */
692 private void updateProfileData()
693 {
694 if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
695 {
696 mProfile.setUsername(mUsername.getText().toString().trim());
697 String password = mPassword.getText().toString().trim();
698 password = password.isEmpty() ? null : password;
699 mProfile.setPassword(password);
700 }
701 if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE))
702 {
703 mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
704 }
705 if (mCertEntry != null)
706 {
707 mProfile.setCertificateAlias(mCertEntry.getAlias());
708 }
709 }
710
711 /**
712 * Load the JSON-encoded VPN profile from the given URI
713 */
714 private static class ProfileLoader extends AsyncTaskLoader<ProfileLoadResult>
715 {
716 private final Uri mUri;
717 private ProfileLoadResult mData;
718
719 public ProfileLoader(Context context, Uri uri)
720 {
721 super(context);
722 mUri = uri;
723 }
724
725 @Override
726 public ProfileLoadResult loadInBackground()
727 {
728 ProfileLoadResult result = new ProfileLoadResult();
729 InputStream in = null;
730
731 if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme()) ||
732 ContentResolver.SCHEME_FILE.equals(mUri.getScheme()))
733 {
734 try
735 {
736 in = getContext().getContentResolver().openInputStream(mUri);
737 }
738 catch (FileNotFoundException e)
739 {
740 result.ThrownException = e;
741 }
742 }
743 else
744 {
745 try
746 {
747 URL url = new URL(mUri.toString());
748 in = url.openStream();
749 }
750 catch (IOException e)
751 {
752 result.ThrownException = e;
753 }
754 }
755 if (in != null)
756 {
757 try
758 {
759 result.Profile = streamToString(in);
760 }
761 catch (OutOfMemoryError e)
762 { /* just use a generic exception */
763 result.ThrownException = new RuntimeException();
764 }
765 }
766 return result;
767 }
768
769 @Override
770 protected void onStartLoading()
771 {
772 if (mData != null)
773 { /* if we have data ready, deliver it directly */
774 deliverResult(mData);
775 }
776 if (takeContentChanged() || mData == null)
777 {
778 forceLoad();
779 }
780 }
781
782 @Override
783 public void deliverResult(ProfileLoadResult data)
784 {
785 if (isReset())
786 {
787 return;
788 }
789 mData = data;
790 if (isStarted())
791 { /* if it is started we deliver the data directly,
792 * otherwise this is handled in onStartLoading */
793 super.deliverResult(data);
794 }
795 }
796
797 @Override
798 protected void onReset()
799 {
800 mData = null;
801 super.onReset();
802 }
803
804 private String streamToString(InputStream in)
805 {
806 ByteArrayOutputStream out = new ByteArrayOutputStream();
807 byte[] buf = new byte[1024];
808 int len;
809
810 try
811 {
812 while ((len = in.read(buf)) != -1)
813 {
814 out.write(buf, 0, len);
815 }
816 return out.toString("UTF-8");
817 }
818 catch (IOException e)
819 {
820 e.printStackTrace();
821 }
822 return null;
823 }
824 }
825
826 private static class ProfileLoadResult
827 {
828 public String Profile;
829 public Exception ThrownException;
830 }
831
832 /**
833 * Ask the user to select an available certificate.
834 */
835 private class SelectUserCertOnClickListener implements View.OnClickListener, KeyChainAliasCallback
836 {
837 @Override
838 public void onClick(View v)
839 {
840 String alias = null;
841 if (mUserCertEntry != null)
842 {
843 alias = mUserCertEntry.getAlias();
844 mUserCertEntry = null;
845 }
846 else if (mProfile != null)
847 {
848 alias = getString(R.string.profile_cert_alias, mProfile.getName());
849 }
850 KeyChain.choosePrivateKeyAlias(VpnProfileImportActivity.this, this, new String[] { "RSA" }, null, null, -1, alias);
851 }
852
853 @Override
854 public void alias(final String alias)
855 {
856 /* alias() is not called from our main thread */
857 runOnUiThread(new Runnable() {
858 @Override
859 public void run()
860 {
861 mUserCertLoading = alias;
862 updateUserCertView();
863 if (alias != null)
864 { /* otherwise the dialog was canceled, the request denied */
865 getLoaderManager().restartLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
866 }
867 }
868 });
869 }
870 }
871
872 /**
873 * Load the selected user certificate asynchronously. This cannot be done
874 * from the main thread as getCertificateChain() calls back to our main
875 * thread to bind to the KeyChain service resulting in a deadlock.
876 */
877 private static class UserCertificateLoader extends AsyncTaskLoader<TrustedCertificateEntry>
878 {
879 private final String mAlias;
880 private TrustedCertificateEntry mData;
881
882 public UserCertificateLoader(Context context, String alias)
883 {
884 super(context);
885 mAlias = alias;
886 }
887
888 @Override
889 public TrustedCertificateEntry loadInBackground()
890 {
891 X509Certificate[] chain = null;
892 try
893 {
894 chain = KeyChain.getCertificateChain(getContext(), mAlias);
895 }
896 catch (KeyChainException | InterruptedException e)
897 {
898 e.printStackTrace();
899 }
900 if (chain != null && chain.length > 0)
901 {
902 return new TrustedCertificateEntry(mAlias, chain[0]);
903 }
904 return null;
905 }
906
907 @Override
908 protected void onStartLoading()
909 {
910 if (mData != null)
911 { /* if we have data ready, deliver it directly */
912 deliverResult(mData);
913 }
914 if (takeContentChanged() || mData == null)
915 {
916 forceLoad();
917 }
918 }
919
920 @Override
921 public void deliverResult(TrustedCertificateEntry data)
922 {
923 if (isReset())
924 {
925 return;
926 }
927 mData = data;
928 if (isStarted())
929 { /* if it is started we deliver the data directly,
930 * otherwise this is handled in onStartLoading */
931 super.deliverResult(data);
932 }
933 }
934
935 @Override
936 protected void onReset()
937 {
938 mData = null;
939 super.onReset();
940 }
941 }
942
943 private byte[] decodeBase64(String encoded)
944 {
945 if (encoded == null || encoded.isEmpty())
946 {
947 return null;
948 }
949 byte[] data = null;
950 try
951 {
952 data = Base64.decode(encoded, Base64.DEFAULT);
953 }
954 catch (IllegalArgumentException e)
955 {
956 e.printStackTrace();
957 }
958 return data;
959 }
960
961 private class ParsedVpnProfile extends VpnProfile
962 {
963 public byte[] Certificate;
964 public byte[] PKCS12;
965 }
966 }