package eu.faircode.email; /* This file is part of FairEmail. FairEmail is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. FairEmail is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with FairEmail. If not, see . Copyright 2018-2022 by Marcel Bokhorst (M66B) */ import android.text.TextUtils; import android.util.Base64; import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.Index; import androidx.room.PrimaryKey; import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.DEROctetString; import org.bouncycastle.asn1.x500.AttributeTypeAndValue; import org.bouncycastle.asn1.x500.RDN; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.GeneralName; import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayInputStream; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Objects; import javax.security.auth.x500.X500Principal; @Entity( tableName = EntityCertificate.TABLE_NAME, foreignKeys = { }, indices = { @Index(value = {"fingerprint", "email"}, unique = true), @Index(value = {"email"}), } ) public class EntityCertificate { static final String TABLE_NAME = "certificate"; @PrimaryKey(autoGenerate = true) public Long id; @NonNull public String fingerprint; @NonNull public boolean intermediate; @NonNull public String email; public String subject; public Long after; public Long before; @NonNull public String data; static EntityCertificate from(X509Certificate certificate, String email) throws CertificateEncodingException, NoSuchAlgorithmException { return from(certificate, false, email); } static EntityCertificate from(X509Certificate certificate, boolean intermediate, String email) throws CertificateEncodingException, NoSuchAlgorithmException { EntityCertificate record = new EntityCertificate(); record.fingerprint = getFingerprintSha256(certificate); record.intermediate = intermediate; record.email = email; record.subject = getSubject(certificate); Date after = certificate.getNotBefore(); Date before = certificate.getNotAfter(); record.after = (after == null ? null : after.getTime()); record.before = (before == null ? null : before.getTime()); record.data = Base64.encodeToString(certificate.getEncoded(), Base64.NO_WRAP); return record; } X509Certificate getCertificate() throws CertificateException { byte[] encoded = Base64.decode(this.data, Base64.NO_WRAP); return (X509Certificate) CertificateFactory.getInstance("X.509") .generateCertificate(new ByteArrayInputStream(encoded)); } boolean isExpired() { return isExpired(null); } boolean isExpired(Date date) { if (date == null) date = new Date(); long now = date.getTime(); return ((this.after != null && now <= this.after) || (this.before != null && now > this.before)); } static String getFingerprintSha256(X509Certificate certificate) throws CertificateEncodingException, NoSuchAlgorithmException { return Helper.sha256(certificate.getEncoded()); } static String getFingerprintSha1(X509Certificate certificate) throws CertificateEncodingException, NoSuchAlgorithmException { return Helper.sha1(certificate.getEncoded()); } static String getKeyId(X509Certificate certificate) { try { byte[] extension = certificate.getExtensionValue(Extension.subjectKeyIdentifier.getId()); if (extension == null) return null; byte[] bytes = DEROctetString.getInstance(extension).getOctets(); SubjectKeyIdentifier keyId = SubjectKeyIdentifier.getInstance(bytes); return Helper.hex(keyId.getKeyIdentifier()); } catch (Throwable ex) { Log.e(ex); return null; } } static String getKeyFingerprint(X509Certificate certificate) { if (certificate == null) return null; try { String keyId = getKeyId(certificate); String fingerPrint = getFingerprintSha1(certificate); return fingerPrint + (keyId == null ? "" : "/" + keyId); } catch (Throwable ex) { Log.e(ex); return null; } } static String getSubject(X509Certificate certificate) { return certificate.getSubjectX500Principal().getName(X500Principal.RFC2253); } static List getEmailAddresses(X509Certificate certificate) { List result = new ArrayList<>(); try { Collection> altNames = certificate.getSubjectAlternativeNames(); if (altNames != null) for (List altName : altNames) if (altName.get(0).equals(GeneralName.rfc822Name)) result.add((String) altName.get(1)); else Log.i("Alt type=" + altName.get(0) + " data=" + altName.get(1)); } catch (CertificateParsingException ex) { Log.e(ex); } try { X500Name name = new JcaX509CertificateHolder(certificate).getSubject(); if (name != null) { List rdns = new ArrayList<>(); rdns.addAll(Arrays.asList(name.getRDNs(BCStyle.CN))); rdns.addAll(Arrays.asList(name.getRDNs(BCStyle.EmailAddress))); for (RDN rdn : rdns) for (AttributeTypeAndValue tv : rdn.getTypesAndValues()) { ASN1Encodable enc = tv.getValue(); if (enc == null) continue; String email = enc.toString().toLowerCase(Locale.ROOT); if (result.contains(email)) continue; if (!Helper.EMAIL_ADDRESS.matcher(email).matches()) continue; result.add(email); } } } catch (Throwable ex) { Log.e(ex); } return result; } static List getDnsNames(X509Certificate certificate) throws CertificateParsingException { List result = new ArrayList<>(); Collection> altNames = certificate.getSubjectAlternativeNames(); if (altNames == null) return result; for (List altName : altNames) if (altName.get(0).equals(GeneralName.dNSName)) result.add((String) altName.get(1)); return result; } static boolean matches(String server, List names) { for (String name : names) if (matches(server, name)) { Log.i("Trusted server=" + server + " name=" + name); return true; } return false; } private static boolean matches(String server, String name) { if (name.startsWith("*.")) { // Wildcard certificate String domain = name.substring(2); if (TextUtils.isEmpty(domain)) return false; int dot = server.indexOf("."); if (dot < 0) return false; String cdomain = server.substring(dot + 1); if (TextUtils.isEmpty(cdomain)) return false; return domain.equalsIgnoreCase(cdomain); } else return server.equalsIgnoreCase(name); } public JSONObject toJSON() throws JSONException { JSONObject json = new JSONObject(); json.put("id", id); json.put("intermediate", intermediate); json.put("email", email); json.put("data", data); return json; } public static EntityCertificate fromJSON(JSONObject json) throws JSONException, CertificateException, NoSuchAlgorithmException { EntityCertificate certificate = new EntityCertificate(); // id certificate.intermediate = json.optBoolean("intermediate"); certificate.email = json.getString("email"); certificate.data = json.getString("data"); X509Certificate cert = certificate.getCertificate(); certificate.fingerprint = getFingerprintSha256(cert); certificate.subject = getSubject(cert); Date after = cert.getNotBefore(); Date before = cert.getNotAfter(); certificate.after = (after == null ? null : after.getTime()); certificate.before = (before == null ? null : before.getTime()); return certificate; } @Override public boolean equals(Object obj) { if (obj instanceof EntityCertificate) { EntityCertificate other = (EntityCertificate) obj; return (this.fingerprint.equals(other.fingerprint) && this.intermediate == other.intermediate && Objects.equals(this.email, other.email) && Objects.equals(this.subject, other.subject)); } else return false; } }