]> git.ipfire.org Git - thirdparty/strongswan.git/blob
322e21e2c0ab93010c37424f6d4a23e5073cf915
[thirdparty/strongswan.git] /
1 /*
2 * Copyright (C) 2014 Tobias Brunner
3 *
4 * Copyright (C) secunet Security Networks AG
5 *
6 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation; either version 2 of the License, or (at your
9 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * for more details.
15 */
16
17 package org.strongswan.android.ui;
18
19 import android.app.Dialog;
20 import android.content.ActivityNotFoundException;
21 import android.content.DialogInterface;
22 import android.content.Intent;
23 import android.net.Uri;
24 import android.os.Build;
25 import android.os.Bundle;
26 import android.widget.Toast;
27
28 import org.strongswan.android.R;
29 import org.strongswan.android.data.VpnProfileDataSource;
30 import org.strongswan.android.logic.TrustedCertificateManager;
31
32 import java.io.FileNotFoundException;
33 import java.io.InputStream;
34 import java.security.KeyStore;
35 import java.security.cert.CertificateException;
36 import java.security.cert.CertificateFactory;
37 import java.security.cert.X509Certificate;
38
39 import androidx.activity.result.ActivityResultLauncher;
40 import androidx.activity.result.contract.ActivityResultContracts;
41 import androidx.appcompat.app.AlertDialog;
42 import androidx.appcompat.app.AppCompatActivity;
43 import androidx.appcompat.app.AppCompatDialogFragment;
44 import androidx.fragment.app.FragmentTransaction;
45
46 public class TrustedCertificateImportActivity extends AppCompatActivity
47 {
48 private static final String DIALOG_TAG = "Dialog";
49 private Uri mCertificateUri;
50
51 private final ActivityResultLauncher<Intent> mOpenDocument = registerForActivityResult(
52 new ActivityResultContracts.StartActivityForResult(),
53 result -> {
54 if (result.getResultCode() == RESULT_OK && result.getData() != null)
55 {
56 mCertificateUri = result.getData().getData();
57 return;
58 }
59 finish();
60 }
61 );
62
63 @Override
64 public void onCreate(Bundle savedInstanceState)
65 {
66 super.onCreate(savedInstanceState);
67
68 if (savedInstanceState != null)
69 { /* do nothing when we are restoring */
70 return;
71 }
72
73 Intent intent = getIntent();
74 String action = intent.getAction();
75 if (Intent.ACTION_VIEW.equals(action))
76 {
77 importCertificate(intent.getData());
78 }
79 else
80 {
81 Intent openIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
82 openIntent.setType("*/*");
83 try
84 {
85 mOpenDocument.launch(openIntent);
86 }
87 catch (ActivityNotFoundException e)
88 { /* some devices are unable to browse for files */
89 finish();
90 }
91 }
92 }
93
94 @Override
95 protected void onPostResume()
96 {
97 super.onPostResume();
98 if (mCertificateUri != null)
99 {
100 importCertificate(mCertificateUri);
101 mCertificateUri = null;
102 }
103 }
104
105 /**
106 * Import the file pointed to by the given URI as a certificate.
107 *
108 * @param uri
109 */
110 private void importCertificate(Uri uri)
111 {
112 X509Certificate certificate = parseCertificate(uri);
113 if (certificate == null)
114 {
115 Toast.makeText(this, R.string.cert_import_failed, Toast.LENGTH_LONG).show();
116 finish();
117 return;
118 }
119 /* Ask the user whether to import the certificate. This is particularly
120 * necessary because the import activity can be triggered by any app on
121 * the system. Also, if our app is the only one that is registered to
122 * open certificate files by MIME type the user would have no idea really
123 * where the file was imported just by reading the Toast we display. */
124 ConfirmImportDialog dialog = new ConfirmImportDialog();
125 Bundle args = new Bundle();
126 args.putSerializable(VpnProfileDataSource.KEY_CERTIFICATE, certificate);
127 dialog.setArguments(args);
128 FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
129 ft.add(dialog, DIALOG_TAG);
130 ft.commit();
131 }
132
133 /**
134 * Load the file from the given URI and try to parse it as X.509 certificate.
135 *
136 * @param uri
137 * @return certificate or null
138 */
139 private X509Certificate parseCertificate(Uri uri)
140 {
141 X509Certificate certificate = null;
142 try
143 {
144 CertificateFactory factory = CertificateFactory.getInstance("X.509");
145 InputStream in = getContentResolver().openInputStream(uri);
146 certificate = (X509Certificate)factory.generateCertificate(in);
147 /* we don't check whether it's actually a CA certificate or not */
148 }
149 catch (CertificateException e)
150 {
151 e.printStackTrace();
152 }
153 catch (FileNotFoundException e)
154 {
155 e.printStackTrace();
156 }
157 return certificate;
158 }
159
160
161 /**
162 * Try to store the given certificate in the KeyStore.
163 *
164 * @param certificate
165 * @return whether it was successfully stored
166 */
167 private boolean storeCertificate(X509Certificate certificate)
168 {
169 try
170 {
171 KeyStore store = KeyStore.getInstance("LocalCertificateStore");
172 store.load(null, null);
173 store.setCertificateEntry(null, certificate);
174 TrustedCertificateManager.getInstance().reset();
175 return true;
176 }
177 catch (Exception e)
178 {
179 e.printStackTrace();
180 return false;
181 }
182 }
183
184 /**
185 * Class that displays a confirmation dialog when a certificate should get
186 * imported. If the user confirms the import we try to store it.
187 */
188 public static class ConfirmImportDialog extends AppCompatDialogFragment
189 {
190 @Override
191 public Dialog onCreateDialog(Bundle savedInstanceState)
192 {
193 final X509Certificate certificate;
194
195 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
196 {
197 certificate = getCertificateCompat(getArguments());
198 }
199 else
200 {
201 certificate = getArguments().getSerializable(VpnProfileDataSource.KEY_CERTIFICATE, X509Certificate.class);
202 }
203
204 return new AlertDialog.Builder(getActivity())
205 .setIcon(R.mipmap.ic_app)
206 .setTitle(R.string.import_certificate)
207 .setMessage(certificate.getSubjectDN().toString())
208 .setPositiveButton(R.string.import_certificate, new DialogInterface.OnClickListener()
209 {
210 @Override
211 public void onClick(DialogInterface dialog, int whichButton)
212 {
213 TrustedCertificateImportActivity activity = (TrustedCertificateImportActivity)getActivity();
214 if (activity.storeCertificate(certificate))
215 {
216 Toast.makeText(getActivity(), R.string.cert_imported_successfully, Toast.LENGTH_LONG).show();
217 getActivity().setResult(RESULT_OK);
218 }
219 else
220 {
221 Toast.makeText(getActivity(), R.string.cert_import_failed, Toast.LENGTH_LONG).show();
222 }
223 getActivity().finish();
224 }
225 })
226 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener()
227 {
228 @Override
229 public void onClick(DialogInterface dialog, int which)
230 {
231 getActivity().finish();
232 }
233 }).create();
234 }
235
236 @Override
237 public void onCancel(DialogInterface dialog)
238 {
239 getActivity().finish();
240 }
241
242 @SuppressWarnings("deprecation")
243 private static X509Certificate getCertificateCompat(Bundle bundle)
244 {
245 return (X509Certificate)bundle.getSerializable(VpnProfileDataSource.KEY_CERTIFICATE);
246 }
247 }
248 }