]>
Commit | Line | Data |
---|---|---|
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 | ||
16 | package org.strongswan.android.ui; | |
17 | ||
18 | import android.app.Activity; | |
19 | import android.app.LoaderManager; | |
b1b626a1 | 20 | import android.content.ActivityNotFoundException; |
3107634e TB |
21 | import android.content.AsyncTaskLoader; |
22 | import android.content.ContentResolver; | |
23 | import android.content.Context; | |
3107634e TB |
24 | import android.content.Intent; |
25 | import android.content.Loader; | |
26 | import android.net.Uri; | |
6766c852 | 27 | import android.os.Build; |
3107634e TB |
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; | |
d134ae21 | 34 | import android.text.TextUtils; |
3107634e TB |
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 | ||
d134ae21 | 47 | import org.json.JSONArray; |
3107634e TB |
48 | import org.json.JSONException; |
49 | import org.json.JSONObject; | |
50 | import org.strongswan.android.R; | |
51 | import org.strongswan.android.data.VpnProfile; | |
d134ae21 | 52 | import org.strongswan.android.data.VpnProfile.SelectedAppsHandling; |
3107634e TB |
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; | |
70f7eb76 | 60 | import org.strongswan.android.utils.IPRangeSet; |
b03713ad | 61 | import org.strongswan.android.utils.Utils; |
3107634e TB |
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; | |
1a39c3d9 | 68 | import java.net.InetAddress; |
3107634e | 69 | import java.net.URL; |
9c79af8c | 70 | import java.net.UnknownHostException; |
3107634e TB |
71 | import java.security.KeyStore; |
72 | import java.security.KeyStoreException; | |
73 | import java.security.NoSuchAlgorithmException; | |
74 | import java.security.cert.CertificateException; | |
75 | import java.security.cert.CertificateFactory; | |
76 | import java.security.cert.X509Certificate; | |
d134ae21 | 77 | import java.util.ArrayList; |
1a39c3d9 | 78 | import java.util.Collections; |
3107634e TB |
79 | import java.util.UUID; |
80 | ||
9c79af8c TB |
81 | import javax.net.ssl.SSLHandshakeException; |
82 | ||
3107634e TB |
83 | public 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 | } |