1/* MemorizingTrustManager - a TrustManager which asks the user about invalid
2 * certificates and memorizes their decision.
3 *
4 * Copyright (c) 2010 Georg Lukas <georg@op-co.de>
5 *
6 * MemorizingTrustManager.java contains the actual trust manager and interface
7 * code to create a MemorizingActivity and obtain the results.
8 *
9 * Permission is hereby granted, free of charge, to any person obtaining a copy
10 * of this software and associated documentation files (the "Software"), to deal
11 * in the Software without restriction, including without limitation the rights
12 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 * copies of the Software, and to permit persons to whom the Software is
14 * furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included in
17 * all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 * THE SOFTWARE.
26 */
27package eu.siacs.conversations.services;
28
29import android.app.Application;
30import android.app.NotificationManager;
31import android.app.Service;
32import android.content.Context;
33import android.content.Intent;
34import android.content.SharedPreferences;
35import android.net.Uri;
36import android.os.Handler;
37import android.preference.PreferenceManager;
38import android.util.Base64;
39import android.util.Log;
40import android.util.SparseArray;
41
42import androidx.appcompat.app.AppCompatActivity;
43import androidx.core.util.Consumer;
44
45import com.google.common.base.Charsets;
46import com.google.common.base.Joiner;
47import com.google.common.io.ByteStreams;
48import com.google.common.io.CharStreams;
49
50import org.json.JSONArray;
51import org.json.JSONException;
52import org.json.JSONObject;
53
54import org.minidns.dane.DaneVerifier;
55
56import java.io.File;
57import java.io.FileInputStream;
58import java.io.FileOutputStream;
59import java.io.IOException;
60import java.io.InputStream;
61import java.io.InputStreamReader;
62import java.security.KeyStore;
63import java.security.KeyStoreException;
64import java.security.MessageDigest;
65import java.security.NoSuchAlgorithmException;
66import java.security.cert.Certificate;
67import java.security.cert.CertificateEncodingException;
68import java.security.cert.CertificateException;
69import java.security.cert.CertificateParsingException;
70import java.security.cert.X509Certificate;
71import java.text.SimpleDateFormat;
72import java.util.ArrayList;
73import java.util.Enumeration;
74import java.util.List;
75import java.util.Locale;
76import java.util.logging.Level;
77import java.util.logging.Logger;
78import java.util.regex.Pattern;
79
80import javax.net.ssl.TrustManager;
81import javax.net.ssl.TrustManagerFactory;
82import javax.net.ssl.X509TrustManager;
83
84import eu.siacs.conversations.Config;
85import eu.siacs.conversations.R;
86import eu.siacs.conversations.crypto.XmppDomainVerifier;
87import eu.siacs.conversations.entities.MTMDecision;
88import eu.siacs.conversations.http.HttpConnectionManager;
89import eu.siacs.conversations.persistance.FileBackend;
90import eu.siacs.conversations.ui.MemorizingActivity;
91
92/**
93 * A X509 trust manager implementation which asks the user about invalid
94 * certificates and memorizes their decision.
95 * <p>
96 * The certificate validity is checked using the system default X509
97 * TrustManager, creating a query Dialog if the check fails.
98 * <p>
99 * <b>WARNING:</b> This only works if a dedicated thread is used for
100 * opening sockets!
101 */
102public class MemorizingTrustManager {
103
104 private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
105
106 final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
107 public final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
108 public final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
109 public final static String DECISION_TITLE_ID = DECISION_INTENT + ".titleId";
110 final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found.";
111 private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
112 private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
113 private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
114 private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
115 private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
116 private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
117 static String KEYSTORE_DIR = "KeyStore";
118 static String KEYSTORE_FILE = "KeyStore.bks";
119 private static int decisionId = 0;
120 private static final SparseArray<MTMDecision> openDecisions = new SparseArray<MTMDecision>();
121 Context master;
122 AppCompatActivity foregroundAct;
123 NotificationManager notificationManager;
124 Handler masterHandler;
125 private File keyStoreFile;
126 private KeyStore appKeyStore;
127 private final X509TrustManager defaultTrustManager;
128 private X509TrustManager appTrustManager;
129 private final DaneVerifier daneVerifier;
130
131 /**
132 * Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager.
133 * <p>
134 * You need to supply the application context. This has to be one of:
135 * - Application
136 * - Activity
137 * - Service
138 * <p>
139 * The context is used for file management, to display the dialog /
140 * notification and for obtaining translated strings.
141 *
142 * @param m Context for the application.
143 * @param defaultTrustManager Delegate trust management to this TM. If null, the user must accept every certificate.
144 */
145 public MemorizingTrustManager(Context m, X509TrustManager defaultTrustManager) {
146 init(m);
147 this.appTrustManager = getTrustManager(appKeyStore);
148 this.defaultTrustManager = defaultTrustManager;
149 this.daneVerifier = new DaneVerifier();
150 }
151
152 /**
153 * Creates an instance of the MemorizingTrustManager class using the system X509TrustManager.
154 * <p>
155 * You need to supply the application context. This has to be one of:
156 * - Application
157 * - Activity
158 * - Service
159 * <p>
160 * The context is used for file management, to display the dialog /
161 * notification and for obtaining translated strings.
162 *
163 * @param m Context for the application.
164 */
165 public MemorizingTrustManager(Context m) {
166 init(m);
167 this.appTrustManager = getTrustManager(appKeyStore);
168 this.defaultTrustManager = getTrustManager(null);
169 this.daneVerifier = new DaneVerifier();
170 }
171
172 private static boolean isIp(final String server) {
173 return server != null && (
174 PATTERN_IPV4.matcher(server).matches()
175 || PATTERN_IPV6.matcher(server).matches()
176 || PATTERN_IPV6_6HEX4DEC.matcher(server).matches()
177 || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches()
178 || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches());
179 }
180
181 private static String getBase64Hash(X509Certificate certificate, String digest) throws CertificateEncodingException {
182 MessageDigest md;
183 try {
184 md = MessageDigest.getInstance(digest);
185 } catch (NoSuchAlgorithmException e) {
186 return null;
187 }
188 md.update(certificate.getEncoded());
189 return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
190 }
191
192 private static String hexString(byte[] data) {
193 StringBuffer si = new StringBuffer();
194 for (int i = 0; i < data.length; i++) {
195 si.append(String.format("%02x", data[i]));
196 if (i < data.length - 1)
197 si.append(":");
198 }
199 return si.toString();
200 }
201
202 private static String certHash(final X509Certificate cert, String digest) {
203 try {
204 MessageDigest md = MessageDigest.getInstance(digest);
205 md.update(cert.getEncoded());
206 return hexString(md.digest());
207 } catch (CertificateEncodingException | NoSuchAlgorithmException e) {
208 return e.getMessage();
209 }
210 }
211
212 public static void interactResult(int decisionId, int choice) {
213 MTMDecision d;
214 synchronized (openDecisions) {
215 d = openDecisions.get(decisionId);
216 openDecisions.remove(decisionId);
217 }
218 if (d == null) {
219 LOGGER.log(Level.SEVERE, "interactResult: aborting due to stale decision reference!");
220 return;
221 }
222 synchronized (d) {
223 d.state = choice;
224 d.notify();
225 }
226 }
227
228 void init(final Context m) {
229 master = m;
230 masterHandler = new Handler(m.getMainLooper());
231 notificationManager = (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE);
232
233 Application app;
234 if (m instanceof Application) {
235 app = (Application) m;
236 } else if (m instanceof Service) {
237 app = ((Service) m).getApplication();
238 } else if (m instanceof AppCompatActivity) {
239 app = ((AppCompatActivity) m).getApplication();
240 } else
241 throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
242
243 File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
244 keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
245
246 appKeyStore = loadAppKeyStore();
247 }
248
249 /**
250 * Get a list of all certificate aliases stored in MTM.
251 *
252 * @return an {@link Enumeration} of all certificates
253 */
254 public Enumeration<String> getCertificates() {
255 try {
256 return appKeyStore.aliases();
257 } catch (KeyStoreException e) {
258 // this should never happen, however...
259 throw new RuntimeException(e);
260 }
261 }
262
263 /**
264 * Removes the given certificate from MTMs key store.
265 *
266 * <p>
267 * <b>WARNING</b>: this does not immediately invalidate the certificate. It is
268 * well possible that (a) data is transmitted over still existing connections or
269 * (b) new connections are created using TLS renegotiation, without a new cert
270 * check.
271 * </p>
272 *
273 * @param alias the certificate's alias as returned by {@link #getCertificates()}.
274 * @throws KeyStoreException if the certificate could not be deleted.
275 */
276 public void deleteCertificate(String alias) throws KeyStoreException {
277 appKeyStore.deleteEntry(alias);
278 keyStoreUpdated();
279 }
280
281 X509TrustManager getTrustManager(KeyStore ks) {
282 try {
283 TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
284 tmf.init(ks);
285 for (TrustManager t : tmf.getTrustManagers()) {
286 if (t instanceof X509TrustManager) {
287 return (X509TrustManager) t;
288 }
289 }
290 } catch (Exception e) {
291 // Here, we are covering up errors. It might be more useful
292 // however to throw them out of the constructor so the
293 // embedding app knows something went wrong.
294 LOGGER.log(Level.SEVERE, "getTrustManager(" + ks + ")", e);
295 }
296 return null;
297 }
298
299 KeyStore loadAppKeyStore() {
300 KeyStore ks;
301 try {
302 ks = KeyStore.getInstance(KeyStore.getDefaultType());
303 } catch (KeyStoreException e) {
304 LOGGER.log(Level.SEVERE, "getAppKeyStore()", e);
305 return null;
306 }
307 FileInputStream fileInputStream = null;
308 try {
309 ks.load(null, null);
310 fileInputStream = new FileInputStream(keyStoreFile);
311 ks.load(fileInputStream, "MTM".toCharArray());
312 } catch (java.io.FileNotFoundException e) {
313 LOGGER.log(Level.INFO, "getAppKeyStore(" + keyStoreFile + ") - file does not exist");
314 } catch (Exception e) {
315 LOGGER.log(Level.SEVERE, "getAppKeyStore(" + keyStoreFile + ")", e);
316 } finally {
317 FileBackend.close(fileInputStream);
318 }
319 return ks;
320 }
321
322 void storeCert(String alias, Certificate cert) {
323 try {
324 appKeyStore.setCertificateEntry(alias, cert);
325 } catch (KeyStoreException e) {
326 LOGGER.log(Level.SEVERE, "storeCert(" + cert + ")", e);
327 return;
328 }
329 keyStoreUpdated();
330 }
331
332 void storeCert(X509Certificate cert) {
333 storeCert(cert.getSubjectDN().toString(), cert);
334 }
335
336 void keyStoreUpdated() {
337 // reload appTrustManager
338 appTrustManager = getTrustManager(appKeyStore);
339
340 // store KeyStore to file
341 java.io.FileOutputStream fos = null;
342 try {
343 fos = new java.io.FileOutputStream(keyStoreFile);
344 appKeyStore.store(fos, "MTM".toCharArray());
345 } catch (Exception e) {
346 LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e);
347 } finally {
348 if (fos != null) {
349 try {
350 fos.close();
351 } catch (IOException e) {
352 LOGGER.log(Level.SEVERE, "storeCert(" + keyStoreFile + ")", e);
353 }
354 }
355 }
356 }
357
358 // if the certificate is stored in the app key store, it is considered "known"
359 private boolean isCertKnown(X509Certificate cert) {
360 try {
361 return appKeyStore.getCertificateAlias(cert) != null;
362 } catch (KeyStoreException e) {
363 return false;
364 }
365 }
366
367
368 private void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive, String verifiedHostname, int port, Consumer<Boolean> daneCb)
369 throws CertificateException {
370 LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
371 try {
372 LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager");
373 if (isServer) {
374 if (verifiedHostname != null) {
375 if (daneVerifier.verifyCertificateChain(chain, verifiedHostname, port)) {
376 if (daneCb != null) daneCb.accept(true);
377 return;
378 }
379 }
380 appTrustManager.checkServerTrusted(chain, authType);
381 } else
382 appTrustManager.checkClientTrusted(chain, authType);
383 } catch (final CertificateException ae) {
384 LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
385 if (isCertKnown(chain[0])) {
386 LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
387 return;
388 }
389 try {
390 if (defaultTrustManager == null)
391 throw ae;
392 LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager");
393 if (isServer)
394 defaultTrustManager.checkServerTrusted(chain, authType);
395 else
396 defaultTrustManager.checkClientTrusted(chain, authType);
397 } catch (final CertificateException e) {
398 if (interactive) {
399 interactCert(chain, authType, e);
400 } else {
401 throw e;
402 }
403 }
404 }
405 }
406
407 private X509Certificate[] getAcceptedIssuers() {
408 return defaultTrustManager == null ? new X509Certificate[0] : defaultTrustManager.getAcceptedIssuers();
409 }
410
411 private int createDecisionId(MTMDecision d) {
412 int myId;
413 synchronized (openDecisions) {
414 myId = decisionId;
415 openDecisions.put(myId, d);
416 decisionId += 1;
417 }
418 return myId;
419 }
420
421 private void certDetails(final StringBuffer si, final X509Certificate c, final boolean showValidFor) {
422
423 si.append("\n");
424 if (showValidFor) {
425 try {
426 si.append("Valid for: ");
427 si.append(Joiner.on(", ").join(XmppDomainVerifier.parseValidDomains(c).all()));
428 } catch (final CertificateParsingException e) {
429 si.append("Unable to parse Certificate");
430 }
431 si.append("\n");
432 } else {
433 si.append(c.getSubjectDN());
434 }
435 si.append("\n");
436 si.append(DATE_FORMAT.format(c.getNotBefore()));
437 si.append(" - ");
438 si.append(DATE_FORMAT.format(c.getNotAfter()));
439 si.append("\nSHA-256: ");
440 si.append(certHash(c, "SHA-256"));
441 si.append("\nSHA-1: ");
442 si.append(certHash(c, "SHA-1"));
443 si.append("\nSigned by: ");
444 si.append(c.getIssuerDN().toString());
445 si.append("\n");
446 }
447
448 private String certChainMessage(final X509Certificate[] chain, CertificateException cause) {
449 Throwable e = cause;
450 LOGGER.log(Level.FINE, "certChainMessage for " + e);
451 final StringBuffer si = new StringBuffer();
452 if (e.getCause() != null) {
453 e = e.getCause();
454 // HACK: there is no sane way to check if the error is a "trust anchor
455 // not found", so we use string comparison.
456 if (NO_TRUST_ANCHOR.equals(e.getMessage())) {
457 si.append(master.getString(R.string.mtm_trust_anchor));
458 } else
459 si.append(e.getLocalizedMessage());
460 si.append("\n");
461 }
462 si.append("\n");
463 si.append(master.getString(R.string.mtm_connect_anyway));
464 si.append("\n\n");
465 si.append(master.getString(R.string.mtm_cert_details));
466 si.append('\n');
467 for(int i = 0; i < chain.length; ++i) {
468 certDetails(si, chain[i], i == 0);
469 }
470 return si.toString();
471 }
472
473 /**
474 * Returns the top-most entry of the activity stack.
475 *
476 * @return the Context of the currently bound UI or the master context if none is bound
477 */
478 Context getUI() {
479 return (foregroundAct != null) ? foregroundAct : master;
480 }
481
482 int interact(final String message, final int titleId) {
483 /* prepare the MTMDecision blocker object */
484 MTMDecision choice = new MTMDecision();
485 final int myId = createDecisionId(choice);
486
487 masterHandler.post(new Runnable() {
488 public void run() {
489 Intent ni = new Intent(master, MemorizingActivity.class);
490 ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
491 ni.setData(Uri.parse(MemorizingTrustManager.class.getName() + "/" + myId));
492 ni.putExtra(DECISION_INTENT_ID, myId);
493 ni.putExtra(DECISION_INTENT_CERT, message);
494 ni.putExtra(DECISION_TITLE_ID, titleId);
495
496 // we try to directly start the activity and fall back to
497 // making a notification
498 try {
499 getUI().startActivity(ni);
500 } catch (Exception e) {
501 LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e);
502 }
503 }
504 });
505
506 LOGGER.log(Level.FINE, "openDecisions: " + openDecisions + ", waiting on " + myId);
507 try {
508 synchronized (choice) {
509 choice.wait();
510 }
511 } catch (InterruptedException e) {
512 LOGGER.log(Level.FINER, "InterruptedException", e);
513 }
514 LOGGER.log(Level.FINE, "finished wait on " + myId + ": " + choice.state);
515 return choice.state;
516 }
517
518 void interactCert(final X509Certificate[] chain, String authType, CertificateException cause)
519 throws CertificateException {
520 switch (interact(certChainMessage(chain, cause), R.string.mtm_accept_cert)) {
521 case MTMDecision.DECISION_ALWAYS:
522 storeCert(chain[0]); // only store the server cert, not the whole chain
523 case MTMDecision.DECISION_ONCE:
524 break;
525 default:
526 throw (cause);
527 }
528 }
529
530 public X509TrustManager getNonInteractive(String domain, String verifiedHostname, int port, Consumer<Boolean> daneCb) {
531 return new NonInteractiveMemorizingTrustManager(domain, verifiedHostname, port, daneCb);
532 }
533
534 public X509TrustManager getInteractive(String domain, String verifiedHostname, int port, Consumer<Boolean> daneCb) {
535 return new InteractiveMemorizingTrustManager(domain, verifiedHostname, port, daneCb);
536 }
537
538 public X509TrustManager getNonInteractive() {
539 return new NonInteractiveMemorizingTrustManager(null, null, 0, null);
540 }
541
542 public X509TrustManager getInteractive() {
543 return new InteractiveMemorizingTrustManager(null, null, 0, null);
544 }
545
546 private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
547
548 private final String domain;
549 private final String verifiedHostname;
550 private final int port;
551 private final Consumer<Boolean> daneCb;
552
553 public NonInteractiveMemorizingTrustManager(String domain, String verifiedHostname, int port, Consumer<Boolean> daneCb) {
554 this.domain = domain;
555 this.verifiedHostname = verifiedHostname;
556 this.port = port;
557 this.daneCb = daneCb;
558 }
559
560 @Override
561 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
562 MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false, verifiedHostname, port, daneCb);
563 }
564
565 @Override
566 public void checkServerTrusted(X509Certificate[] chain, String authType)
567 throws CertificateException {
568 MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false, verifiedHostname, port, daneCb);
569 }
570
571 @Override
572 public X509Certificate[] getAcceptedIssuers() {
573 return MemorizingTrustManager.this.getAcceptedIssuers();
574 }
575
576 }
577
578 private class InteractiveMemorizingTrustManager implements X509TrustManager {
579 private final String domain;
580 private final String verifiedHostname;
581 private final int port;
582 private final Consumer<Boolean> daneCb;
583
584 public InteractiveMemorizingTrustManager(String domain, String verifiedHostname, int port, Consumer<Boolean> daneCb) {
585 this.domain = domain;
586 this.verifiedHostname = verifiedHostname;
587 this.port = port;
588 this.daneCb = daneCb;
589 }
590
591 @Override
592 public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
593 MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true, verifiedHostname, port, daneCb);
594 }
595
596 @Override
597 public void checkServerTrusted(X509Certificate[] chain, String authType)
598 throws CertificateException {
599 MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true, verifiedHostname, port, daneCb);
600 }
601
602 @Override
603 public X509Certificate[] getAcceptedIssuers() {
604 return MemorizingTrustManager.this.getAcceptedIssuers();
605 }
606 }
607}