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