android: Import NAT-T keepalive interval
[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
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 ProgressDialog mProgress;
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 mExistsWarning = (TextView)findViewById(R.id.exists_warning);
169 mBasicDataGroup = (ViewGroup)findViewById(R.id.basic_data_group);
170 mName = (TextView)findViewById(R.id.name);
171 mGateway = (TextView)findViewById(R.id.gateway);
172 mSelectVpnType = (TextView)findViewById(R.id.vpn_type);
173
174 mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
175 mUsername = (EditText)findViewById(R.id.username);
176 mUsernameWrap = (TextInputLayoutHelper) findViewById(R.id.username_wrap);
177 mPassword = (EditText)findViewById(R.id.password);
178
179 mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group);
180 mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate);
181 mImportUserCert = (Button)findViewById(R.id.import_user_certificate);
182
183 mRemoteCertificate = (ViewGroup)findViewById(R.id.remote_certificate_group);
184 mRemoteCert = (RelativeLayout)findViewById(R.id.remote_certificate);
185
186 mExistsWarning.setVisibility(View.GONE);
187 mBasicDataGroup.setVisibility(View.GONE);
188 mUsernamePassword.setVisibility(View.GONE);
189 mUserCertificate.setVisibility(View.GONE);
190 mRemoteCertificate.setVisibility(View.GONE);
191
192 mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
193 mImportUserCert.setOnClickListener(new View.OnClickListener() {
194 @Override
195 public void onClick(View v)
196 {
197 Intent intent = KeyChain.createInstallIntent();
198 intent.putExtra(KeyChain.EXTRA_NAME, getString(R.string.profile_cert_alias, mProfile.getName()));
199 intent.putExtra(KeyChain.EXTRA_PKCS12, mProfile.PKCS12);
200 startActivityForResult(intent, INSTALL_PKCS12);
201 }
202 });
203
204 Intent intent = getIntent();
205 String action = intent.getAction();
206 if (Intent.ACTION_VIEW.equals(action))
207 {
208 loadProfile(getIntent().getData());
209 }
210 else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
211 {
212 Intent openIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
213 openIntent.setType("*/*");
214 startActivityForResult(openIntent, OPEN_DOCUMENT);
215 }
216
217 if (savedInstanceState != null)
218 {
219 mUserCertLoading = savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
220 if (mUserCertLoading != null)
221 {
222 getLoaderManager().initLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
223 }
224 mImportUserCert.setEnabled(!savedInstanceState.getBoolean(PKCS12_INSTALLED));
225 }
226 }
227
228 @Override
229 protected void onDestroy()
230 {
231 super.onDestroy();
232 mDataSource.close();
233 }
234
235 @Override
236 protected void onSaveInstanceState(Bundle outState)
237 {
238 super.onSaveInstanceState(outState);
239 if (mUserCertEntry != null)
240 {
241 outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
242 }
243 outState.putBoolean(PKCS12_INSTALLED, !mImportUserCert.isEnabled());
244 }
245
246 @Override
247 public boolean onCreateOptionsMenu(Menu menu)
248 {
249 MenuInflater inflater = getMenuInflater();
250 inflater.inflate(R.menu.profile_import, menu);
251 if (mHideImport)
252 {
253 MenuItem item = menu.findItem(R.id.menu_accept);
254 item.setVisible(false);
255 }
256 return true;
257 }
258
259 @Override
260 public boolean onOptionsItemSelected(MenuItem item)
261 {
262 switch (item.getItemId())
263 {
264 case android.R.id.home:
265 finish();
266 return true;
267 case R.id.menu_accept:
268 saveProfile();
269 return true;
270 default:
271 return super.onOptionsItemSelected(item);
272 }
273 }
274
275 @Override
276 protected void onActivityResult(int requestCode, int resultCode, Intent data)
277 {
278 super.onActivityResult(requestCode, resultCode, data);
279 switch (requestCode)
280 {
281 case INSTALL_PKCS12:
282 if (resultCode == Activity.RESULT_OK)
283 { /* no need to import twice */
284 mImportUserCert.setEnabled(false);
285 mSelectUserCert.performClick();
286 }
287 break;
288 case OPEN_DOCUMENT:
289 if (resultCode == Activity.RESULT_OK && data != null)
290 {
291 loadProfile(data.getData());
292 return;
293 }
294 finish();
295 break;
296 }
297 }
298
299 private void loadProfile(Uri uri)
300 {
301 mProgress = ProgressDialog.show(this, null, getString(R.string.loading),
302 true, true, new DialogInterface.OnCancelListener() {
303 @Override
304 public void onCancel(DialogInterface dialog)
305 {
306 finish();
307 }
308 });
309
310 Bundle args = new Bundle();
311 args.putParcelable(PROFILE_URI, uri);
312 getLoaderManager().initLoader(PROFILE_LOADER, args, mProfileLoaderCallbacks);
313 }
314
315 public void handleProfile(ProfileLoadResult data)
316 {
317 mProgress.dismiss();
318
319 mProfile = null;
320 if (data != null && data.ThrownException == null)
321 {
322 try
323 {
324 JSONObject obj = new JSONObject(data.Profile);
325 mProfile = parseProfile(obj);
326 }
327 catch (JSONException e)
328 {
329 mExistsWarning.setVisibility(View.VISIBLE);
330 mExistsWarning.setText(e.getLocalizedMessage());
331 mHideImport = true;
332 invalidateOptionsMenu();
333 return;
334 }
335 }
336 if (mProfile == null)
337 {
338 String error = null;
339 if (data.ThrownException != null)
340 {
341 try
342 {
343 throw data.ThrownException;
344 }
345 catch (FileNotFoundException e)
346 {
347 error = getString(R.string.profile_import_failed_not_found);
348 }
349 catch (UnknownHostException e)
350 {
351 error = getString(R.string.profile_import_failed_host);
352 }
353 catch (SSLHandshakeException e)
354 {
355 error = getString(R.string.profile_import_failed_tls);
356 }
357 catch (Exception e)
358 {
359 e.printStackTrace();
360 }
361 }
362 if (error != null)
363 {
364 Toast.makeText(this, getString(R.string.profile_import_failed_detail, error), Toast.LENGTH_LONG).show();
365 }
366 else
367 {
368 Toast.makeText(this, R.string.profile_import_failed, Toast.LENGTH_LONG).show();
369 }
370 finish();
371 return;
372 }
373 mExisting = mDataSource.getVpnProfile(mProfile.getUUID());
374 mExistsWarning.setVisibility(mExisting != null ? View.VISIBLE : View.GONE);
375
376 mBasicDataGroup.setVisibility(View.VISIBLE);
377 mName.setText(mProfile.getName());
378 mGateway.setText(mProfile.getGateway());
379 mSelectVpnType.setText(getResources().getStringArray(R.array.vpn_types)[mProfile.getVpnType().ordinal()]);
380
381 mUsernamePassword.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE);
382 if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
383 {
384 mUsername.setText(mProfile.getUsername());
385 if (mProfile.getUsername() != null && !mProfile.getUsername().isEmpty())
386 {
387 mUsername.setEnabled(false);
388 }
389 }
390
391 mUserCertificate.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE);
392 mRemoteCertificate.setVisibility(mProfile.Certificate != null ? View.VISIBLE : View.GONE);
393 mImportUserCert.setVisibility(mProfile.PKCS12 != null ? View.VISIBLE : View.GONE);
394
395 if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE))
396 { /* try to load an existing certificate with the default name */
397 if (mUserCertLoading == null)
398 {
399 mUserCertLoading = getString(R.string.profile_cert_alias, mProfile.getName());
400 getLoaderManager().initLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
401 }
402 updateUserCertView();
403 }
404
405 if (mProfile.Certificate != null)
406 {
407 try
408 {
409 CertificateFactory factory = CertificateFactory.getInstance("X.509");
410 X509Certificate certificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(mProfile.Certificate));
411 KeyStore store = KeyStore.getInstance("LocalCertificateStore");
412 store.load(null, null);
413 String alias = store.getCertificateAlias(certificate);
414 mCertEntry = new TrustedCertificateEntry(alias, certificate);
415 ((TextView)mRemoteCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
416 ((TextView)mRemoteCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
417 }
418 catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e)
419 {
420 e.printStackTrace();
421 mRemoteCertificate.setVisibility(View.GONE);
422 }
423 }
424 }
425
426 private void handleUserCertificate(TrustedCertificateEntry data)
427 {
428 mUserCertEntry = data;
429 mUserCertLoading = null;
430 updateUserCertView();
431 }
432
433 private void updateUserCertView()
434 {
435 if (mUserCertLoading != null)
436 {
437 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
438 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
439 }
440 else if (mUserCertEntry != null)
441 { /* clear any errors and set the new data */
442 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
443 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
444 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
445 }
446 else
447 {
448 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label);
449 ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate);
450 }
451 }
452
453 private ParsedVpnProfile parseProfile(JSONObject obj) throws JSONException
454 {
455 UUID uuid;
456 try
457 {
458 uuid = UUID.fromString(obj.getString("uuid"));
459 }
460 catch (IllegalArgumentException e)
461 {
462 e.printStackTrace();
463 return null;
464 }
465 ParsedVpnProfile profile = new ParsedVpnProfile();
466
467 profile.setUUID(uuid);
468 profile.setName(obj.getString("name"));
469 VpnType type = VpnType.fromIdentifier(obj.getString("type"));
470 profile.setVpnType(type);
471
472 JSONObject remote = obj.getJSONObject("remote");
473 profile.setGateway(remote.getString("addr"));
474 profile.setPort(getInteger(remote, "port", 1, 65535));
475 profile.setRemoteId(remote.optString("id", null));
476 profile.Certificate = decodeBase64(remote.optString("cert", null));
477
478 JSONObject local = obj.optJSONObject("local");
479 if (local != null)
480 {
481 if (type.has(VpnTypeFeature.USER_PASS))
482 {
483 profile.setUsername(local.optString("eap_id", null));
484 }
485
486 if (type.has(VpnTypeFeature.CERTIFICATE))
487 {
488 profile.setLocalId(local.optString("id", null));
489 profile.PKCS12 = decodeBase64(local.optString("p12", null));
490 }
491 }
492
493 profile.setMTU(getInteger(obj, "mtu", Constants.MTU_MIN, Constants.MTU_MAX));
494 profile.setNATKeepAlive(getInteger(obj, "nat-keepalive", Constants.NAT_KEEPALIVE_MIN, Constants.NAT_KEEPALIVE_MAX));
495 JSONObject split = obj.optJSONObject("split-tunneling");
496 if (split != null)
497 {
498 String included = getSubnets(split, "subnets");
499 profile.setIncludedSubnets(included != null ? included : null);
500 String excluded = getSubnets(split, "excluded");
501 profile.setExcludedSubnets(excluded != null ? excluded : null);
502 int st = 0;
503 st |= split.optBoolean("block-ipv4") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
504 st |= split.optBoolean("block-ipv6") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
505 profile.setSplitTunneling(st == 0 ? null : st);
506 }
507 /* only one of these can be set, prefer specific apps */
508 String selectedApps = getApps(obj.optJSONArray("apps"));
509 String excludedApps = getApps(obj.optJSONArray("excluded-apps"));
510 if (!TextUtils.isEmpty(selectedApps))
511 {
512 profile.setSelectedApps(selectedApps);
513 profile.setSelectedAppsHandling(SelectedAppsHandling.SELECTED_APPS_ONLY);
514 }
515 else if (!TextUtils.isEmpty(excludedApps))
516 {
517 profile.setSelectedApps(excludedApps);
518 profile.setSelectedAppsHandling(SelectedAppsHandling.SELECTED_APPS_EXCLUDE);
519 }
520 return profile;
521 }
522
523 private Integer getInteger(JSONObject obj, String key, int min, int max)
524 {
525 Integer res = obj.optInt(key);
526 return res < min || res > max ? null : res;
527 }
528
529 private String getSubnets(JSONObject split, String key) throws JSONException
530 {
531 ArrayList<String> subnets = new ArrayList<>();
532 JSONArray arr = split.optJSONArray(key);
533 if (arr != null)
534 {
535 for (int i = 0; i < arr.length(); i++)
536 { /* replace all spaces, e.g. in "192.168.1.1 - 192.168.1.10" */
537 subnets.add(arr.getString(i).replace(" ", ""));
538 }
539 }
540 else
541 {
542 String value = split.optString(key, null);
543 if (!TextUtils.isEmpty(value))
544 {
545 subnets.add(value);
546 }
547 }
548 if (subnets.size() > 0)
549 {
550 String joined = TextUtils.join(" ", subnets);
551 IPRangeSet ranges = IPRangeSet.fromString(joined);
552 if (ranges == null)
553 {
554 throw new JSONException(getString(R.string.profile_import_failed_value,
555 "split-tunneling." + key));
556 }
557 return ranges.toString();
558 }
559 return null;
560 }
561
562 private String getApps(JSONArray arr) throws JSONException
563 {
564 ArrayList<String> apps = new ArrayList<>();
565 if (arr != null)
566 {
567 for (int i = 0; i < arr.length(); i++)
568 {
569 apps.add(arr.getString(i));
570 }
571 }
572 return TextUtils.join(" ", apps);
573 }
574
575 /**
576 * Save or update the profile depending on whether we actually have a
577 * profile object or not (this was created in updateProfileData)
578 */
579 private void saveProfile()
580 {
581 if (verifyInput())
582 {
583 updateProfileData();
584 if (mExisting != null)
585 {
586 mProfile.setId(mExisting.getId());
587 mDataSource.updateVpnProfile(mProfile);
588 }
589 else
590 {
591 mDataSource.insertProfile(mProfile);
592 }
593 if (mCertEntry != null)
594 {
595 try
596 { /* store the CA/server certificate */
597 KeyStore store = KeyStore.getInstance("LocalCertificateStore");
598 store.load(null, null);
599 store.setCertificateEntry(null, mCertEntry.getCertificate());
600 TrustedCertificateManager.getInstance().reset();
601 }
602 catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e)
603 {
604 e.printStackTrace();
605 }
606 }
607 Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED);
608 intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId());
609 LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
610
611 intent = new Intent(this, MainActivity.class);
612 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
613 startActivity(intent);
614
615 setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
616 finish();
617 }
618 }
619
620 /**
621 * Verify the user input and display error messages.
622 * @return true if the input is valid
623 */
624 private boolean verifyInput()
625 {
626 boolean valid = true;
627 if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
628 {
629 if (mUsername.getText().toString().trim().isEmpty())
630 {
631 mUsernameWrap.setError(getString(R.string.alert_text_no_input_username));
632 valid = false;
633 }
634 }
635 if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null)
636 { /* let's show an error icon */
637 ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
638 valid = false;
639 }
640 return valid;
641 }
642
643 /**
644 * Update the profile object with the data entered by the user
645 */
646 private void updateProfileData()
647 {
648 if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
649 {
650 mProfile.setUsername(mUsername.getText().toString().trim());
651 String password = mPassword.getText().toString().trim();
652 password = password.isEmpty() ? null : password;
653 mProfile.setPassword(password);
654 }
655 if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE))
656 {
657 mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
658 }
659 if (mCertEntry != null)
660 {
661 mProfile.setCertificateAlias(mCertEntry.getAlias());
662 }
663 }
664
665 /**
666 * Load the JSON-encoded VPN profile from the given URI
667 */
668 private static class ProfileLoader extends AsyncTaskLoader<ProfileLoadResult>
669 {
670 private final Uri mUri;
671 private ProfileLoadResult mData;
672
673 public ProfileLoader(Context context, Uri uri)
674 {
675 super(context);
676 mUri = uri;
677 }
678
679 @Override
680 public ProfileLoadResult loadInBackground()
681 {
682 ProfileLoadResult result = new ProfileLoadResult();
683 InputStream in = null;
684
685 if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme()) ||
686 ContentResolver.SCHEME_FILE.equals(mUri.getScheme()))
687 {
688 try
689 {
690 in = getContext().getContentResolver().openInputStream(mUri);
691 }
692 catch (FileNotFoundException e)
693 {
694 result.ThrownException = e;
695 }
696 }
697 else
698 {
699 try
700 {
701 URL url = new URL(mUri.toString());
702 in = url.openStream();
703 }
704 catch (IOException e)
705 {
706 result.ThrownException = e;
707 }
708 }
709 if (in != null)
710 {
711 result.Profile = streamToString(in);
712 }
713 return result;
714 }
715
716 @Override
717 protected void onStartLoading()
718 {
719 if (mData != null)
720 { /* if we have data ready, deliver it directly */
721 deliverResult(mData);
722 }
723 if (takeContentChanged() || mData == null)
724 {
725 forceLoad();
726 }
727 }
728
729 @Override
730 public void deliverResult(ProfileLoadResult data)
731 {
732 if (isReset())
733 {
734 return;
735 }
736 mData = data;
737 if (isStarted())
738 { /* if it is started we deliver the data directly,
739 * otherwise this is handled in onStartLoading */
740 super.deliverResult(data);
741 }
742 }
743
744 @Override
745 protected void onReset()
746 {
747 mData = null;
748 super.onReset();
749 }
750
751 private String streamToString(InputStream in)
752 {
753 ByteArrayOutputStream out = new ByteArrayOutputStream();
754 byte[] buf = new byte[1024];
755 int len;
756
757 try
758 {
759 while ((len = in.read(buf)) != -1)
760 {
761 out.write(buf, 0, len);
762 }
763 return out.toString("UTF-8");
764 }
765 catch (IOException e)
766 {
767 e.printStackTrace();
768 }
769 return null;
770 }
771 }
772
773 private static class ProfileLoadResult
774 {
775 public String Profile;
776 public Exception ThrownException;
777 }
778
779 /**
780 * Ask the user to select an available certificate.
781 */
782 private class SelectUserCertOnClickListener implements View.OnClickListener, KeyChainAliasCallback
783 {
784 @Override
785 public void onClick(View v)
786 {
787 String alias = null;
788 if (mUserCertEntry != null)
789 {
790 alias = mUserCertEntry.getAlias();
791 mUserCertEntry = null;
792 }
793 else if (mProfile != null)
794 {
795 alias = getString(R.string.profile_cert_alias, mProfile.getName());
796 }
797 KeyChain.choosePrivateKeyAlias(VpnProfileImportActivity.this, this, new String[] { "RSA" }, null, null, -1, alias);
798 }
799
800 @Override
801 public void alias(final String alias)
802 {
803 /* alias() is not called from our main thread */
804 runOnUiThread(new Runnable() {
805 @Override
806 public void run()
807 {
808 mUserCertLoading = alias;
809 updateUserCertView();
810 if (alias != null)
811 { /* otherwise the dialog was canceled, the request denied */
812 getLoaderManager().restartLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
813 }
814 }
815 });
816 }
817 }
818
819 /**
820 * Load the selected user certificate asynchronously. This cannot be done
821 * from the main thread as getCertificateChain() calls back to our main
822 * thread to bind to the KeyChain service resulting in a deadlock.
823 */
824 private static class UserCertificateLoader extends AsyncTaskLoader<TrustedCertificateEntry>
825 {
826 private final String mAlias;
827 private TrustedCertificateEntry mData;
828
829 public UserCertificateLoader(Context context, String alias)
830 {
831 super(context);
832 mAlias = alias;
833 }
834
835 @Override
836 public TrustedCertificateEntry loadInBackground()
837 {
838 X509Certificate[] chain = null;
839 try
840 {
841 chain = KeyChain.getCertificateChain(getContext(), mAlias);
842 }
843 catch (KeyChainException | InterruptedException e)
844 {
845 e.printStackTrace();
846 }
847 if (chain != null && chain.length > 0)
848 {
849 return new TrustedCertificateEntry(mAlias, chain[0]);
850 }
851 return null;
852 }
853
854 @Override
855 protected void onStartLoading()
856 {
857 if (mData != null)
858 { /* if we have data ready, deliver it directly */
859 deliverResult(mData);
860 }
861 if (takeContentChanged() || mData == null)
862 {
863 forceLoad();
864 }
865 }
866
867 @Override
868 public void deliverResult(TrustedCertificateEntry data)
869 {
870 if (isReset())
871 {
872 return;
873 }
874 mData = data;
875 if (isStarted())
876 { /* if it is started we deliver the data directly,
877 * otherwise this is handled in onStartLoading */
878 super.deliverResult(data);
879 }
880 }
881
882 @Override
883 protected void onReset()
884 {
885 mData = null;
886 super.onReset();
887 }
888 }
889
890 private byte[] decodeBase64(String encoded)
891 {
892 if (encoded == null || encoded.isEmpty())
893 {
894 return null;
895 }
896 byte[] data = null;
897 try
898 {
899 data = Base64.decode(encoded, Base64.DEFAULT);
900 }
901 catch (IllegalArgumentException e)
902 {
903 e.printStackTrace();
904 }
905 return data;
906 }
907
908 private class ParsedVpnProfile extends VpnProfile
909 {
910 public byte[] Certificate;
911 public byte[] PKCS12;
912 }
913 }