]> git.ipfire.org Git - thirdparty/strongswan.git/blob - src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java
43c0035cf920c03832d539bb753bbde92c552f02
[thirdparty/strongswan.git] / src / frontends / android / app / src / main / java / org / strongswan / android / ui / VpnProfileImportActivity.java
1 /*
2 * Copyright (C) 2016-2017 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.app.ProgressDialog;
21 import android.content.AsyncTaskLoader;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.content.DialogInterface;
25 import android.content.Intent;
26 import android.content.Loader;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.security.KeyChain;
31 import android.security.KeyChainAliasCallback;
32 import android.security.KeyChainException;
33 import android.support.v4.content.LocalBroadcastManager;
34 import android.support.v7.app.AppCompatActivity;
35 import android.text.TextUtils;
36 import android.util.Base64;
37 import android.view.Menu;
38 import android.view.MenuInflater;
39 import android.view.MenuItem;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.widget.Button;
43 import android.widget.EditText;
44 import android.widget.RelativeLayout;
45 import android.widget.TextView;
46 import android.widget.Toast;
47
48 import org.json.JSONArray;
49 import org.json.JSONException;
50 import org.json.JSONObject;
51 import org.strongswan.android.R;
52 import org.strongswan.android.data.VpnProfile;
53 import org.strongswan.android.data.VpnProfile.SelectedAppsHandling;
54 import org.strongswan.android.data.VpnProfileDataSource;
55 import org.strongswan.android.data.VpnType;
56 import org.strongswan.android.data.VpnType.VpnTypeFeature;
57 import org.strongswan.android.logic.TrustedCertificateManager;
58 import org.strongswan.android.security.TrustedCertificateEntry;
59 import org.strongswan.android.ui.widget.TextInputLayoutHelper;
60 import org.strongswan.android.utils.Constants;
61 import org.strongswan.android.utils.IPRangeSet;
62 import org.strongswan.android.utils.Utils;
63
64 import java.io.ByteArrayInputStream;
65 import java.io.ByteArrayOutputStream;
66 import java.io.FileNotFoundException;
67 import java.io.IOException;
68 import java.io.InputStream;
69 import java.lang.OutOfMemoryError;
70 import java.net.URL;
71 import java.net.UnknownHostException;
72 import java.security.KeyStore;
73 import java.security.KeyStoreException;
74 import java.security.NoSuchAlgorithmException;
75 import java.security.cert.CertificateException;
76 import java.security.cert.CertificateFactory;
77 import java.security.cert.X509Certificate;
78 import java.util.ArrayList;
79 import java.util.UUID;
80
81 import javax.net.ssl.SSLHandshakeException;
82
83 public class VpnProfileImportActivity extends AppCompatActivity
84 {
85 private static final String PKCS12_INSTALLED = "PKCS12_INSTALLED";
86 private static final String PROFILE_URI = "PROFILE_URI";
87 private static final int INSTALL_PKCS12 = 0;
88 private static final int OPEN_DOCUMENT = 1;
89 private static final int PROFILE_LOADER = 0;
90 private static final int USER_CERT_LOADER = 1;
91
92 private VpnProfileDataSource mDataSource;
93 private ParsedVpnProfile mProfile;
94 private VpnProfile mExisting;
95 private TrustedCertificateEntry mCertEntry;
96 private TrustedCertificateEntry mUserCertEntry;
97 private String mUserCertLoading;
98 private boolean mHideImport;
99 private ProgressDialog mProgress;
100 private TextView mExistsWarning;
101 private ViewGroup mBasicDataGroup;
102 private TextView mName;
103 private TextView mGateway;
104 private TextView mSelectVpnType;
105 private ViewGroup mUsernamePassword;
106 private EditText mUsername;
107 private TextInputLayoutHelper mUsernameWrap;
108 private EditText mPassword;
109 private ViewGroup mUserCertificate;
110 private RelativeLayout mSelectUserCert;
111 private Button mImportUserCert;
112 private ViewGroup mRemoteCertificate;
113 private RelativeLayout mRemoteCert;
114
115 private LoaderManager.LoaderCallbacks<ProfileLoadResult> mProfileLoaderCallbacks = new LoaderManager.LoaderCallbacks<ProfileLoadResult>()
116 {
117 @Override
118 public Loader<ProfileLoadResult> onCreateLoader(int id, Bundle args)
119 {
120 return new ProfileLoader(VpnProfileImportActivity.this, (Uri)args.getParcelable(PROFILE_URI));
121 }
122
123 @Override
124 public void onLoadFinished(Loader<ProfileLoadResult> loader, ProfileLoadResult data)
125 {
126 handleProfile(data);
127 }
128
129 @Override
130 public void onLoaderReset(Loader<ProfileLoadResult> loader)
131 {
132
133 }
134 };
135
136 private LoaderManager.LoaderCallbacks<TrustedCertificateEntry> mUserCertificateLoaderCallbacks = new LoaderManager.LoaderCallbacks<TrustedCertificateEntry>()
137 {
138 @Override
139 public Loader<TrustedCertificateEntry> onCreateLoader(int id, Bundle args)
140 {
141 return new UserCertificateLoader(VpnProfileImportActivity.this, mUserCertLoading);
142 }
143
144 @Override
145 public void onLoadFinished(Loader<TrustedCertificateEntry> loader, TrustedCertificateEntry data)
146 {
147 handleUserCertificate(data);
148 }
149
150 @Override
151 public void onLoaderReset(Loader<TrustedCertificateEntry> loader)
152 {
153
154 }
155 };
156
157 @Override
158 public void onCreate(Bundle savedInstanceState)
159 {
160 super.onCreate(savedInstanceState);
161
162 getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
163 getSupportActionBar().setDisplayHomeAsUpEnabled(true);
164
165 mDataSource = new VpnProfileDataSource(this);
166 mDataSource.open();
167
168 setContentView(R.layout.profile_import_view);
169
170 mExistsWarning = (TextView)findViewById(R.id.exists_warning);
171 mBasicDataGroup = (ViewGroup)findViewById(R.id.basic_data_group);
172 mName = (TextView)findViewById(R.id.name);
173 mGateway = (TextView)findViewById(R.id.gateway);
174 mSelectVpnType = (TextView)findViewById(R.id.vpn_type);
175
176 mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
177 mUsername = (EditText)findViewById(R.id.username);
178 mUsernameWrap = (TextInputLayoutHelper) findViewById(R.id.username_wrap);
179 mPassword = (EditText)findViewById(R.id.password);
180
181 mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group);
182 mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate);
183 mImportUserCert = (Button)findViewById(R.id.import_user_certificate);
184
185 mRemoteCertificate = (ViewGroup)findViewById(R.id.remote_certificate_group);
186 mRemoteCert = (RelativeLayout)findViewById(R.id.remote_certificate);
187
188 mExistsWarning.setVisibility(View.GONE);
189 mBasicDataGroup.setVisibility(View.GONE);
190 mUsernamePassword.setVisibility(View.GONE);
191 mUserCertificate.setVisibility(View.GONE);
192 mRemoteCertificate.setVisibility(View.GONE);
193
194 mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
195 mImportUserCert.setOnClickListener(new View.OnClickListener() {
196 @Override
197 public void onClick(View v)
198 {
199 Intent intent = KeyChain.createInstallIntent();
200 intent.putExtra(KeyChain.EXTRA_NAME, getString(R.string.profile_cert_alias, mProfile.getName()));
201 intent.putExtra(KeyChain.EXTRA_PKCS12, mProfile.PKCS12);
202 startActivityForResult(intent, INSTALL_PKCS12);
203 }
204 });
205
206 Intent intent = getIntent();
207 String action = intent.getAction();
208 if (Intent.ACTION_VIEW.equals(action))
209 {
210 loadProfile(getIntent().getData());
211 }
212 else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
213 {
214 Intent openIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
215 openIntent.setType("*/*");
216 startActivityForResult(openIntent, OPEN_DOCUMENT);
217 }
218
219 if (savedInstanceState != null)
220 {
221 mUserCertLoading = savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
222 if (mUserCertLoading != null)
223 {
224 getLoaderManager().initLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
225 }
226 mImportUserCert.setEnabled(!savedInstanceState.getBoolean(PKCS12_INSTALLED));
227 }
228 }
229
230 @Override
231 protected void onDestroy()
232 {
233 super.onDestroy();
234 mDataSource.close();
235 }
236
237 @Override
238 protected void onSaveInstanceState(Bundle outState)
239 {
240 super.onSaveInstanceState(outState);
241 if (mUserCertEntry != null)
242 {
243 outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
244 }
245 outState.putBoolean(PKCS12_INSTALLED, !mImportUserCert.isEnabled());
246 }
247
248 @Override
249 public boolean onCreateOptionsMenu(Menu menu)
250 {
251 MenuInflater inflater = getMenuInflater();
252 inflater.inflate(R.menu.profile_import, menu);
253 if (mHideImport)
254 {
255 MenuItem item = menu.findItem(R.id.menu_accept);
256 item.setVisible(false);
257 }
258 return true;
259 }
260
261 @Override
262 public boolean onOptionsItemSelected(MenuItem item)
263 {
264 switch (item.getItemId())
265 {
266 case android.R.id.home:
267 finish();
268 return true;
269 case R.id.menu_accept:
270 saveProfile();
271 return true;
272 default:
273 return super.onOptionsItemSelected(item);
274 }
275 }
276
277 @Override
278 protected void onActivityResult(int requestCode, int resultCode, Intent data)
279 {
280 super.onActivityResult(requestCode, resultCode, data);
281 switch (requestCode)
282 {
283 case INSTALL_PKCS12:
284 if (resultCode == Activity.RESULT_OK)
285 { /* no need to import twice */
286 mImportUserCert.setEnabled(false);
287 mSelectUserCert.performClick();
288 }
289 break;
290 case OPEN_DOCUMENT:
291 if (resultCode == Activity.RESULT_OK && data != null)
292 {
293 loadProfile(data.getData());
294 return;
295 }
296 finish();
297 break;
298 }
299 }
300
301 private void loadProfile(Uri uri)
302 {
303 mProgress = ProgressDialog.show(this, null, getString(R.string.loading),
304 true, true, new DialogInterface.OnCancelListener() {
305 @Override
306 public void onCancel(DialogInterface dialog)
307 {
308 finish();
309 }
310 });
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 mProgress.dismiss();
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", false))
482 {
483 flags |= VpnProfile.FLAGS_SUPPRESS_CERT_REQS;
484 }
485
486 JSONObject local = obj.optJSONObject("local");
487 if (local != null)
488 {
489 if (type.has(VpnTypeFeature.USER_PASS))
490 {
491 profile.setUsername(local.optString("eap_id", null));
492 }
493
494 if (type.has(VpnTypeFeature.CERTIFICATE))
495 {
496 profile.setLocalId(local.optString("id", null));
497 profile.PKCS12 = decodeBase64(local.optString("p12", null));
498 }
499 }
500
501 profile.setIkeProposal(getProposal(obj, "ike-proposal", true));
502 profile.setEspProposal(getProposal(obj, "esp-proposal", false));
503 profile.setMTU(getInteger(obj, "mtu", Constants.MTU_MIN, Constants.MTU_MAX));
504 profile.setNATKeepAlive(getInteger(obj, "nat-keepalive", Constants.NAT_KEEPALIVE_MIN, Constants.NAT_KEEPALIVE_MAX));
505 JSONObject split = obj.optJSONObject("split-tunneling");
506 if (split != null)
507 {
508 String included = getSubnets(split, "subnets");
509 profile.setIncludedSubnets(included != null ? included : null);
510 String excluded = getSubnets(split, "excluded");
511 profile.setExcludedSubnets(excluded != null ? excluded : null);
512 int st = 0;
513 st |= split.optBoolean("block-ipv4") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
514 st |= split.optBoolean("block-ipv6") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
515 profile.setSplitTunneling(st == 0 ? null : st);
516 }
517 /* only one of these can be set, prefer specific apps */
518 String selectedApps = getApps(obj.optJSONArray("apps"));
519 String excludedApps = getApps(obj.optJSONArray("excluded-apps"));
520 if (!TextUtils.isEmpty(selectedApps))
521 {
522 profile.setSelectedApps(selectedApps);
523 profile.setSelectedAppsHandling(SelectedAppsHandling.SELECTED_APPS_ONLY);
524 }
525 else if (!TextUtils.isEmpty(excludedApps))
526 {
527 profile.setSelectedApps(excludedApps);
528 profile.setSelectedAppsHandling(SelectedAppsHandling.SELECTED_APPS_EXCLUDE);
529 }
530 profile.setFlags(flags);
531 return profile;
532 }
533
534 private Integer getInteger(JSONObject obj, String key, int min, int max)
535 {
536 Integer res = obj.optInt(key);
537 return res < min || res > max ? null : res;
538 }
539
540 private String getProposal(JSONObject obj, String key, boolean ike) throws JSONException
541 {
542 String value = obj.optString(key, null);
543 if (!TextUtils.isEmpty(value))
544 {
545 if (!Utils.isProposalValid(ike, value))
546 {
547 throw new JSONException(getString(R.string.profile_import_failed_value, key));
548 }
549 }
550 return value;
551 }
552
553 private String getSubnets(JSONObject split, String key) throws JSONException
554 {
555 ArrayList<String> subnets = new ArrayList<>();
556 JSONArray arr = split.optJSONArray(key);
557 if (arr != null)
558 {
559 for (int i = 0; i < arr.length(); i++)
560 { /* replace all spaces, e.g. in "192.168.1.1 - 192.168.1.10" */
561 subnets.add(arr.getString(i).replace(" ", ""));
562 }
563 }
564 else
565 {
566 String value = split.optString(key, null);
567 if (!TextUtils.isEmpty(value))
568 {
569 subnets.add(value);
570 }
571 }
572 if (subnets.size() > 0)
573 {
574 String joined = TextUtils.join(" ", subnets);
575 IPRangeSet ranges = IPRangeSet.fromString(joined);
576 if (ranges == null)
577 {
578 throw new JSONException(getString(R.string.profile_import_failed_value,
579 "split-tunneling." + key));
580 }
581 return ranges.toString();
582 }
583 return null;
584 }
585
586 private String getApps(JSONArray arr) throws JSONException
587 {
588 ArrayList<String> apps = new ArrayList<>();
589 if (arr != null)
590 {
591 for (int i = 0; i < arr.length(); i++)
592 {
593 apps.add(arr.getString(i));
594 }
595 }
596 return TextUtils.join(" ", apps);
597 }
598
599 /**
600 * Save or update the profile depending on whether we actually have a
601 * profile object or not (this was created in updateProfileData)
602 */
603 private void saveProfile()
604 {
605 if (verifyInput())
606 {
607 updateProfileData();
608 if (mExisting != null)
609 {
610 mProfile.setId(mExisting.getId());
611 mDataSource.updateVpnProfile(mProfile);
612 }
613 else
614 {
615 mDataSource.insertProfile(mProfile);
616 }
617 if (mCertEntry != null)
618 {
619 try
620 { /* store the CA/server certificate */
621 KeyStore store = KeyStore.getInstance("LocalCertificateStore");
622 store.load(null, null);
623 store.setCertificateEntry(null, mCertEntry.getCertificate());
624 TrustedCertificateManager.getInstance().reset();
625 }
626 catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e)
627 {
628 e.printStackTrace();
629 }
630 }
631 Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED);
632 intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId());
633 LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
634
635 intent = new Intent(this, MainActivity.class);
636 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
637 startActivity(intent);
638
639 setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
640 finish();
641 }
642 }
643
644 /**
645 * Verify the user input and display error messages.
646 * @return true if the input is valid
647 */
648 private boolean verifyInput()
649 {
650 boolean valid = true;
651 if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
652 {
653 if (mUsername.getText().toString().trim().isEmpty())
654 {
655 mUsernameWrap.setError(getString(R.string.alert_text_no_input_username));
656 valid = false;
657 }
658 }
659 if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null)
660 { /* let's show an error icon */
661 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
662 valid = false;
663 }
664 return valid;
665 }
666
667 /**
668 * Update the profile object with the data entered by the user
669 */
670 private void updateProfileData()
671 {
672 if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
673 {
674 mProfile.setUsername(mUsername.getText().toString().trim());
675 String password = mPassword.getText().toString().trim();
676 password = password.isEmpty() ? null : password;
677 mProfile.setPassword(password);
678 }
679 if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE))
680 {
681 mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
682 }
683 if (mCertEntry != null)
684 {
685 mProfile.setCertificateAlias(mCertEntry.getAlias());
686 }
687 }
688
689 /**
690 * Load the JSON-encoded VPN profile from the given URI
691 */
692 private static class ProfileLoader extends AsyncTaskLoader<ProfileLoadResult>
693 {
694 private final Uri mUri;
695 private ProfileLoadResult mData;
696
697 public ProfileLoader(Context context, Uri uri)
698 {
699 super(context);
700 mUri = uri;
701 }
702
703 @Override
704 public ProfileLoadResult loadInBackground()
705 {
706 ProfileLoadResult result = new ProfileLoadResult();
707 InputStream in = null;
708
709 if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme()) ||
710 ContentResolver.SCHEME_FILE.equals(mUri.getScheme()))
711 {
712 try
713 {
714 in = getContext().getContentResolver().openInputStream(mUri);
715 }
716 catch (FileNotFoundException e)
717 {
718 result.ThrownException = e;
719 }
720 }
721 else
722 {
723 try
724 {
725 URL url = new URL(mUri.toString());
726 in = url.openStream();
727 }
728 catch (IOException e)
729 {
730 result.ThrownException = e;
731 }
732 }
733 if (in != null)
734 {
735 try
736 {
737 result.Profile = streamToString(in);
738 }
739 catch (OutOfMemoryError e)
740 { /* just use a generic exception */
741 result.ThrownException = new RuntimeException();
742 }
743 }
744 return result;
745 }
746
747 @Override
748 protected void onStartLoading()
749 {
750 if (mData != null)
751 { /* if we have data ready, deliver it directly */
752 deliverResult(mData);
753 }
754 if (takeContentChanged() || mData == null)
755 {
756 forceLoad();
757 }
758 }
759
760 @Override
761 public void deliverResult(ProfileLoadResult data)
762 {
763 if (isReset())
764 {
765 return;
766 }
767 mData = data;
768 if (isStarted())
769 { /* if it is started we deliver the data directly,
770 * otherwise this is handled in onStartLoading */
771 super.deliverResult(data);
772 }
773 }
774
775 @Override
776 protected void onReset()
777 {
778 mData = null;
779 super.onReset();
780 }
781
782 private String streamToString(InputStream in)
783 {
784 ByteArrayOutputStream out = new ByteArrayOutputStream();
785 byte[] buf = new byte[1024];
786 int len;
787
788 try
789 {
790 while ((len = in.read(buf)) != -1)
791 {
792 out.write(buf, 0, len);
793 }
794 return out.toString("UTF-8");
795 }
796 catch (IOException e)
797 {
798 e.printStackTrace();
799 }
800 return null;
801 }
802 }
803
804 private static class ProfileLoadResult
805 {
806 public String Profile;
807 public Exception ThrownException;
808 }
809
810 /**
811 * Ask the user to select an available certificate.
812 */
813 private class SelectUserCertOnClickListener implements View.OnClickListener, KeyChainAliasCallback
814 {
815 @Override
816 public void onClick(View v)
817 {
818 String alias = null;
819 if (mUserCertEntry != null)
820 {
821 alias = mUserCertEntry.getAlias();
822 mUserCertEntry = null;
823 }
824 else if (mProfile != null)
825 {
826 alias = getString(R.string.profile_cert_alias, mProfile.getName());
827 }
828 KeyChain.choosePrivateKeyAlias(VpnProfileImportActivity.this, this, new String[] { "RSA" }, null, null, -1, alias);
829 }
830
831 @Override
832 public void alias(final String alias)
833 {
834 /* alias() is not called from our main thread */
835 runOnUiThread(new Runnable() {
836 @Override
837 public void run()
838 {
839 mUserCertLoading = alias;
840 updateUserCertView();
841 if (alias != null)
842 { /* otherwise the dialog was canceled, the request denied */
843 getLoaderManager().restartLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
844 }
845 }
846 });
847 }
848 }
849
850 /**
851 * Load the selected user certificate asynchronously. This cannot be done
852 * from the main thread as getCertificateChain() calls back to our main
853 * thread to bind to the KeyChain service resulting in a deadlock.
854 */
855 private static class UserCertificateLoader extends AsyncTaskLoader<TrustedCertificateEntry>
856 {
857 private final String mAlias;
858 private TrustedCertificateEntry mData;
859
860 public UserCertificateLoader(Context context, String alias)
861 {
862 super(context);
863 mAlias = alias;
864 }
865
866 @Override
867 public TrustedCertificateEntry loadInBackground()
868 {
869 X509Certificate[] chain = null;
870 try
871 {
872 chain = KeyChain.getCertificateChain(getContext(), mAlias);
873 }
874 catch (KeyChainException | InterruptedException e)
875 {
876 e.printStackTrace();
877 }
878 if (chain != null && chain.length > 0)
879 {
880 return new TrustedCertificateEntry(mAlias, chain[0]);
881 }
882 return null;
883 }
884
885 @Override
886 protected void onStartLoading()
887 {
888 if (mData != null)
889 { /* if we have data ready, deliver it directly */
890 deliverResult(mData);
891 }
892 if (takeContentChanged() || mData == null)
893 {
894 forceLoad();
895 }
896 }
897
898 @Override
899 public void deliverResult(TrustedCertificateEntry data)
900 {
901 if (isReset())
902 {
903 return;
904 }
905 mData = data;
906 if (isStarted())
907 { /* if it is started we deliver the data directly,
908 * otherwise this is handled in onStartLoading */
909 super.deliverResult(data);
910 }
911 }
912
913 @Override
914 protected void onReset()
915 {
916 mData = null;
917 super.onReset();
918 }
919 }
920
921 private byte[] decodeBase64(String encoded)
922 {
923 if (encoded == null || encoded.isEmpty())
924 {
925 return null;
926 }
927 byte[] data = null;
928 try
929 {
930 data = Base64.decode(encoded, Base64.DEFAULT);
931 }
932 catch (IllegalArgumentException e)
933 {
934 e.printStackTrace();
935 }
936 return data;
937 }
938
939 private class ParsedVpnProfile extends VpnProfile
940 {
941 public byte[] Certificate;
942 public byte[] PKCS12;
943 }
944 }