Check S/MIME key validity

pull/169/head
M66B 6 years ago
parent af8e36e23e
commit 8dd9c62365

File diff suppressed because it is too large Load Diff

@ -1069,11 +1069,7 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac
for (String email : emails) {
EntityCertificate record = db.certificate().getCertificate(fingerprint, email);
if (record == null) {
record = new EntityCertificate();
record.fingerprint = fingerprint;
record.email = email;
record.subject = subject;
record.setCertificate(cert);
record = EntityCertificate.from(cert, email);
record.id = db.certificate().insertCertificate(record);
}
}

@ -20,7 +20,6 @@ package eu.faircode.email;
*/
import android.content.Context;
import android.graphics.Typeface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
@ -37,22 +36,27 @@ import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class AdapterCertificate extends RecyclerView.Adapter<AdapterCertificate.ViewHolder> {
private Context context;
private LifecycleOwner owner;
private LayoutInflater inflater;
private String email;
private List<EntityCertificate> items = new ArrayList<>();
private DateFormat TF;
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnLongClickListener {
private View view;
private TextView tvEmail;
private TextView tvSubject;
private TextView tvAfter;
private TextView tvBefore;
private TextView tvOutdated;
private TwoStateOwner powner = new TwoStateOwner(owner, "CertificatePopup");
@ -62,6 +66,9 @@ public class AdapterCertificate extends RecyclerView.Adapter<AdapterCertificate.
view = itemView.findViewById(R.id.clItem);
tvEmail = itemView.findViewById(R.id.tvEmail);
tvSubject = itemView.findViewById(R.id.tvSubject);
tvAfter = itemView.findViewById(R.id.tvAfter);
tvBefore = itemView.findViewById(R.id.tvBefore);
tvOutdated = itemView.findViewById(R.id.tvOutdated);
}
@Override
@ -130,10 +137,9 @@ public class AdapterCertificate extends RecyclerView.Adapter<AdapterCertificate.
private void bindTo(EntityCertificate certificate) {
tvEmail.setText(certificate.email);
tvSubject.setText(certificate.subject);
boolean preferred = Objects.equals(email, certificate.email);
tvEmail.setTypeface(preferred ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
tvSubject.setTypeface(preferred ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
tvAfter.setText(certificate.after == null ? null : TF.format(certificate.after));
tvBefore.setText(certificate.before == null ? null : TF.format(certificate.before));
tvOutdated.setVisibility(certificate.isOutdated() ? View.VISIBLE : View.GONE);
}
}
@ -142,15 +148,16 @@ public class AdapterCertificate extends RecyclerView.Adapter<AdapterCertificate.
this.owner = parentFragment.getViewLifecycleOwner();
this.inflater = LayoutInflater.from(parentFragment.getContext());
this.TF = Helper.getDateTimeInstance(context, SimpleDateFormat.SHORT, SimpleDateFormat.SHORT);
setHasStableIds(true);
}
public void set(String email, @NonNull List<EntityCertificate> certificates) {
Log.i("Set email=" + email + " certificates=" + certificates.size());
public void set(@NonNull List<EntityCertificate> certificates) {
Log.i("Set certificates=" + certificates.size());
DiffUtil.DiffResult diff = DiffUtil.calculateDiff(new DiffCallback(items, certificates), false);
this.email = email;
this.items = certificates;
diff.dispatchUpdatesTo(new ListUpdateCallback() {

@ -56,7 +56,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory;
// https://developer.android.com/topic/libraries/architecture/room.html
@Database(
version = 119,
version = 120,
entities = {
EntityIdentity.class,
EntityAccount.class,
@ -1162,6 +1162,14 @@ public abstract class DB extends RoomDatabase {
db.execSQL("ALTER TABLE `identity` ADD COLUMN `sign_key_alias` TEXT");
}
})
.addMigrations(new Migration(119, 120) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase db) {
Log.i("DB migration from version " + startVersion + " to " + endVersion);
db.execSQL("ALTER TABLE `certificate` ADD COLUMN `after` INTEGER");
db.execSQL("ALTER TABLE `certificate` ADD COLUMN `before` INTEGER");
}
})
.build();
}

@ -39,6 +39,7 @@ import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@ -63,24 +64,37 @@ public class EntityCertificate {
@NonNull
public String email;
public String subject;
public Long after;
public Long before;
@NonNull
public String data;
private void setEncoded(byte[] encoded) {
this.data = Base64.encodeToString(encoded, Base64.NO_WRAP);
}
static EntityCertificate from(X509Certificate certificate, String email) throws CertificateEncodingException, NoSuchAlgorithmException {
EntityCertificate record = new EntityCertificate();
record.fingerprint = getFingerprint(certificate);
record.email = email;
record.subject = getSubject(certificate);
private byte[] getEncoded() {
return Base64.decode(this.data, Base64.NO_WRAP);
}
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);
void setCertificate(X509Certificate certificate) throws CertificateEncodingException {
setEncoded(certificate.getEncoded());
return record;
}
X509Certificate getCertificate() throws CertificateException {
byte[] encoded = Base64.decode(this.data, Base64.NO_WRAP);
return (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(getEncoded()));
.generateCertificate(new ByteArrayInputStream(encoded));
}
boolean isOutdated() {
long now = new Date().getTime();
return ((this.after != null && now <= this.after) || (this.before != null && now > this.before));
}
static String getFingerprint(X509Certificate certificate) throws CertificateEncodingException, NoSuchAlgorithmException {
@ -126,6 +140,12 @@ public class EntityCertificate {
certificate.fingerprint = getFingerprint(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;
}

@ -100,7 +100,7 @@ public class FragmentCertificates extends FragmentBase {
if (certificates == null)
certificates = new ArrayList<>();
adapter.set(null, certificates);
adapter.set(certificates);
pbWait.setVisibility(View.GONE);
grpReady.setVisibility(View.VISIBLE);

@ -135,6 +135,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.UnknownHostException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@ -1948,6 +1949,11 @@ public class FragmentCompose extends FragmentBase {
X509Certificate[] chain = KeyChain.getCertificateChain(context, alias);
if (chain == null || chain.length == 0)
throw new IllegalArgumentException("Certificate missing");
try {
chain[0].checkValidity();
} catch (CertificateException ex) {
throw new IllegalArgumentException(context.getString(R.string.title_invalid_key), ex);
}
// Build content
if (EntityMessage.SMIME_SIGNONLY.equals(type)) {
@ -2022,13 +2028,25 @@ public class FragmentCompose extends FragmentBase {
List<X509Certificate> certs = new ArrayList<>();
certs.add(chain[0]); // Allow sender to decrypt own message
for (Address address : addresses) {
String email = ((InternetAddress) address).getAddress();
List<EntityCertificate> acertificates = db.certificate().getCertificateByEmail(email);
if (acertificates == null || acertificates.size() == 0)
throw new IllegalArgumentException(context.getString(R.string.title_certificate_missing, email), new IllegalStateException());
for (EntityCertificate acertificate : acertificates)
certs.add(acertificate.getCertificate());
throw new IllegalArgumentException(
context.getString(R.string.title_certificate_missing, email), new CertificateException());
for (EntityCertificate acertificate : acertificates) {
X509Certificate cert = acertificate.getCertificate();
try {
cert.checkValidity();
} catch (CertificateException ex) {
throw new IllegalArgumentException(
context.getString(R.string.title_certificate_invalid, email), ex);
}
certs.add(cert);
}
}
// Build signature
@ -2095,7 +2113,7 @@ public class FragmentCompose extends FragmentBase {
if (ex instanceof IllegalArgumentException) {
Log.i(ex);
Snackbar snackbar = Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG);
if (ex.getCause() instanceof IllegalStateException)
if (ex.getCause() instanceof CertificateException)
snackbar.setAction(R.string.title_fix, new View.OnClickListener() {
@Override
public void onClick(View v) {

@ -4557,87 +4557,98 @@ public class FragmentMessages extends FragmentBase implements SharedPreferences.
String sender = args.getString("sender");
boolean known = args.getBoolean("known");
boolean match = false;
List<String> emails = (cert == null ? Collections.emptyList() : EntityCertificate.getAltSubjectName(cert));
for (String email : emails)
if (Objects.equals(sender, email)) {
match = true;
break;
}
if (cert == null)
Snackbar.make(view, R.string.title_signature_invalid, Snackbar.LENGTH_LONG).show();
else if (known && match)
Snackbar.make(view, R.string.title_signature_valid, Snackbar.LENGTH_LONG).show();
else {
LayoutInflater inflator = LayoutInflater.from(getContext());
View dview = inflator.inflate(R.layout.dialog_certificate, null);
TextView tvSender = dview.findViewById(R.id.tvSender);
TextView tvEmail = dview.findViewById(R.id.tvEmail);
TextView tvEmailInvalid = dview.findViewById(R.id.tvEmailInvalid);
TextView tvSubject = dview.findViewById(R.id.tvSubject);
tvSender.setText(sender);
tvEmail.setText(TextUtils.join(",", emails));
tvEmailInvalid.setVisibility(match ? View.GONE : View.VISIBLE);
tvSubject.setText(EntityCertificate.getSubject(cert));
AlertDialog.Builder builder = new AlertDialog.Builder(getContext())
.setView(dview)
.setNegativeButton(android.R.string.cancel, null);
if (!TextUtils.isEmpty(sender) && !known && emails.size() > 0)
builder.setPositiveButton(R.string.title_signature_store, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
args.putByteArray("encoded", cert.getEncoded());
new SimpleTask<Void>() {
@Override
protected Void onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
byte[] encoded = args.getByteArray("encoded");
X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(encoded));
DB db = DB.getInstance(context);
EntityMessage message = db.message().getMessage(id);
if (message == null)
return null;
String fingerprint = EntityCertificate.getFingerprint(cert);
List<String> emails = EntityCertificate.getAltSubjectName(cert);
String subject = EntityCertificate.getSubject(cert);
for (String email : emails) {
EntityCertificate record = db.certificate().getCertificate(fingerprint, email);
if (record == null) {
record = new EntityCertificate();
record.fingerprint = fingerprint;
record.email = email;
record.subject = subject;
record.setCertificate(cert);
record.id = db.certificate().insertCertificate(record);
}
}
else
try {
EntityCertificate record = EntityCertificate.from(cert, null);
boolean match = false;
List<String> emails = EntityCertificate.getAltSubjectName(cert);
for (String email : emails)
if (Objects.equals(sender, email)) {
match = true;
break;
}
return null;
}
if (known && !record.isOutdated() && match)
Snackbar.make(view, R.string.title_signature_valid, Snackbar.LENGTH_LONG).show();
else {
LayoutInflater inflator = LayoutInflater.from(getContext());
View dview = inflator.inflate(R.layout.dialog_certificate, null);
TextView tvSender = dview.findViewById(R.id.tvSender);
TextView tvEmail = dview.findViewById(R.id.tvEmail);
TextView tvEmailInvalid = dview.findViewById(R.id.tvEmailInvalid);
TextView tvSubject = dview.findViewById(R.id.tvSubject);
TextView tvAfter = dview.findViewById(R.id.tvAfter);
TextView tvBefore = dview.findViewById(R.id.tvBefore);
TextView tvOutdated = dview.findViewById(R.id.tvOutdated);
tvSender.setText(sender);
tvEmail.setText(TextUtils.join(",", emails));
tvEmailInvalid.setVisibility(match ? View.GONE : View.VISIBLE);
tvSubject.setText(record.subject);
DateFormat TF = Helper.getDateTimeInstance(getContext(), SimpleDateFormat.SHORT, SimpleDateFormat.SHORT);
tvAfter.setText(record.after == null ? null : TF.format(record.after));
tvBefore.setText(record.before == null ? null : TF.format(record.before));
tvOutdated.setVisibility(record.isOutdated() ? View.VISIBLE : View.GONE);
AlertDialog.Builder builder = new AlertDialog.Builder(getContext())
.setView(dview)
.setNegativeButton(android.R.string.cancel, null);
if (!TextUtils.isEmpty(sender) && !known && emails.size() > 0)
builder.setPositiveButton(R.string.title_signature_store, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
args.putByteArray("encoded", cert.getEncoded());
new SimpleTask<Void>() {
@Override
protected Void onExecute(Context context, Bundle args) throws Throwable {
long id = args.getLong("id");
byte[] encoded = args.getByteArray("encoded");
X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(encoded));
DB db = DB.getInstance(context);
EntityMessage message = db.message().getMessage(id);
if (message == null)
return null;
String fingerprint = EntityCertificate.getFingerprint(cert);
List<String> emails = EntityCertificate.getAltSubjectName(cert);
String subject = EntityCertificate.getSubject(cert);
for (String email : emails) {
EntityCertificate record = db.certificate().getCertificate(fingerprint, email);
if (record == null) {
record = EntityCertificate.from(cert, email);
record.id = db.certificate().insertCertificate(record);
}
}
return null;
}
@Override
protected void onException(Bundle args, Throwable ex) {
@Override
protected void onException(Bundle args, Throwable ex) {
Log.unexpectedError(getParentFragmentManager(), ex);
}
}.execute(FragmentMessages.this, args, "certificate:store");
} catch (Throwable ex) {
Log.unexpectedError(getParentFragmentManager(), ex);
}
}.execute(FragmentMessages.this, args, "certificate:store");
} catch (Throwable ex) {
Log.unexpectedError(getParentFragmentManager(), ex);
}
}
});
}
});
builder.show();
}
builder.show();
}
} catch (Throwable ex) {
Snackbar.make(view, Log.formatThrowable(ex), Snackbar.LENGTH_LONG).show();
}
}
}

@ -60,7 +60,7 @@
android:id="@+id/tvEmailInvalid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginTop="12dp"
android:text="@string/title_signature_mismatch"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?attr/colorWarning"
@ -85,5 +85,47 @@
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSubjectTitle" />
<TextView
android:id="@+id/tvValidityTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/title_signature_validity"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvSubject" />
<TextView
android:id="@+id/tvAfter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="after"
android:textAppearance="@android:style/TextAppearance.Small"
app:layout_constraintEnd_toStartOf="@+id/tvBefore"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvValidityTitle" />
<TextView
android:id="@+id/tvBefore"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="end"
android:text="before"
android:textAppearance="@android:style/TextAppearance.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tvAfter"
app:layout_constraintTop_toBottomOf="@+id/tvValidityTitle" />
<TextView
android:id="@+id/tvOutdated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_signature_outdated"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?attr/colorWarning"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAfter" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

@ -31,5 +31,36 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvEmail" />
<TextView
android:id="@+id/tvAfter"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="after"
android:textAppearance="@android:style/TextAppearance.Small"
app:layout_constraintEnd_toStartOf="@+id/tvBefore"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvSubject" />
<TextView
android:id="@+id/tvBefore"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="end"
android:text="before"
android:textAppearance="@android:style/TextAppearance.Small"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tvAfter"
app:layout_constraintTop_toBottomOf="@+id/tvSubject" />
<TextView
android:id="@+id/tvOutdated"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_signature_outdated"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?attr/colorWarning"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvAfter" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

@ -679,6 +679,7 @@
<string name="title_edit_formatted_text">Edit as reformatted text</string>
<string name="title_select_certificate">Select public key</string>
<string name="title_certificate_missing">No public key for %1$s</string>
<string name="title_certificate_invalid">Public key of %1$s is invalid</string>
<string name="title_no_key">No private key</string>
<string name="title_invalid_key">Invalid private key</string>
<string name="title_send_plain_text">Plain text only</string>
@ -715,6 +716,8 @@
<string name="title_signature_email">Signature\'s address</string>
<string name="title_signature_mismatch">The email address of the sender and signature do not match</string>
<string name="title_signature_subject">Subject</string>
<string name="title_signature_validity">Validity</string>
<string name="title_signature_outdated">This public key is currently not valid</string>
<string name="title_signature_store">Store</string>
<string name="title_search">Search</string>

Loading…
Cancel
Save