Skip to content
Snippets Groups Projects
Commit 7a1204e0 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Added SSL Handling

parent 42fa2d26
No related branches found
No related tags found
No related merge requests found
Showing
with 339 additions and 31 deletions
......@@ -6,7 +6,6 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.v7.view.ContextThemeWrapper;
import android.util.Log;
import de.kuschku.quasseldroid_ng.R;
import de.kuschku.util.annotationbind.AutoBinder;
......
......@@ -8,4 +8,8 @@ public class ServerAddress {
this.host = host;
this.port = port;
}
public String print() {
return String.format("%s:%s", host, port);
}
}
package de.kuschku.util.certificates;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteStatement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
public class CertificateDatabaseHandler extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "certificates";
private static final String TABLE_CERTIFICATES = "certificates";
private static final String KEY_CORE_ADDRESS = "core_address";
private static final String KEY_FINGERPRINT = "fingerprint";
// Again we can only use String.format, as SQL doesn’t support table or column names to be bound
// in prepared statements
private static final String STATEMENT_INSERT =
String.format("INSERT OR IGNORE INTO %s(%s, %s) VALUES (?, ?)",
TABLE_CERTIFICATES, KEY_CORE_ADDRESS, KEY_FINGERPRINT);
private static final String STATEMENT_DELETE =
String.format("DELETE FROM %s WHERE %s = ? AND %s = ?",
TABLE_CERTIFICATES, KEY_CORE_ADDRESS, KEY_FINGERPRINT);
private static final String STATEMENT_DELETE_ALL =
String.format("DELETE FROM %s WHERE %s = ?",
TABLE_CERTIFICATES, KEY_CORE_ADDRESS);
private static final String SPECIFIER_FIND_ALL = String.format("%s = ?", KEY_CORE_ADDRESS);
public CertificateDatabaseHandler(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// Why do we use String.format and not prepared statements? Because we can’t bind table or
// column names in prepared statements
String statement = String.format("CREATE TABLE %s (%s, %s, PRIMARY KEY (%s, %s), UNIQUE(%s, %s));",
TABLE_CERTIFICATES,
KEY_CORE_ADDRESS, KEY_FINGERPRINT,
KEY_CORE_ADDRESS, KEY_FINGERPRINT,
KEY_CORE_ADDRESS, KEY_FINGERPRINT);
db.execSQL(statement);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
public boolean addCertificate(String fingerprint, String coreAddress) {
SQLiteDatabase db = this.getWritableDatabase();
SQLiteStatement statement = db.compileStatement(STATEMENT_INSERT);
statement.bindString(1, coreAddress);
statement.bindString(2, fingerprint);
// executeInsert returns -1 if unsuccessful
return statement.executeInsert() != -1;
}
public boolean removeCertificate(String fingerprint, String coreAddress) {
SQLiteDatabase db = this.getWritableDatabase();
SQLiteStatement statement = db.compileStatement(STATEMENT_DELETE);
statement.bindString(1, coreAddress);
statement.bindString(2, fingerprint);
// executeUpdateDelete returns amount of modified rows
return statement.executeUpdateDelete() > 0;
}
public boolean removeCertificates(String coreAddress) {
SQLiteDatabase db = this.getWritableDatabase();
SQLiteStatement statement = db.compileStatement(STATEMENT_DELETE_ALL);
statement.bindString(1, coreAddress);
// executeUpdateDelete returns amount of modified rows
return statement.executeUpdateDelete() > 0;
}
public Cursor cursorFindCertificates(String coreAddress) {
SQLiteDatabase db = this.getReadableDatabase();
return db.query(
// table name
TABLE_CERTIFICATES,
// column names
new String[]{KEY_FINGERPRINT},
// where clause
SPECIFIER_FIND_ALL,
// binds for where clause
new String[]{coreAddress},
null,
null,
null,
null
);
}
public Cursor cursorFindAllCertificates() {
SQLiteDatabase db = this.getReadableDatabase();
return db.query(
// table name
TABLE_CERTIFICATES,
// column names
new String[]{KEY_CORE_ADDRESS, KEY_FINGERPRINT},
// where clause
null,
// binds for where clause
new String[0],
null,
null,
null,
null
);
}
public List<String> findCertificates(String coreAddress) {
Cursor cursor = cursorFindCertificates(coreAddress);
List<String> certificates = new ArrayList<>();
if (cursor != null && cursor.moveToFirst()) {
do {
certificates.add(cursor.getString(0));
} while (cursor.moveToNext());
}
return certificates;
}
public Map<String, Collection<String>> findAllCertificates() {
Cursor cursor = cursorFindAllCertificates();
Map<String, Collection<String>> certificates = new HashMap<>();
if (cursor != null && cursor.moveToFirst()) {
do {
String coreid = cursor.getString(0);
String fingerprint = cursor.getString(1);
if (certificates.get(coreid) == null)
certificates.put(coreid, new HashSet<>());
certificates.get(coreid).add(fingerprint);
} while (cursor.moveToNext());
}
return certificates;
}
}
package de.kuschku.util.certificates;
import com.google.common.base.Joiner;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import de.kuschku.util.Objects;
public class CertificateUtils {
private CertificateUtils() {
}
public static String certificateToFingerprint(X509Certificate certificate) throws NoSuchAlgorithmException, CertificateEncodingException {
return hashToFingerprint(getHash(certificate));
}
public static String certificateToFingerprint(X509Certificate certificate, String defaultValue) {
try {
return certificateToFingerprint(certificate);
} catch (Exception e) {
return defaultValue;
}
}
private static byte[] getHash(X509Certificate certificate) throws NoSuchAlgorithmException, CertificateEncodingException {
MessageDigest digest = java.security.MessageDigest.getInstance("SHA1");
digest.update(certificate.getEncoded());
return digest.digest();
}
public static String hashToFingerprint(byte[] hash) {
String[] formattedBytes = new String[hash.length];
for (int i = 0; i < hash.length; i++) {
// Format each byte as hex string
formattedBytes[i] = Integer.toHexString(hash[i] & 0xff);
}
return Joiner.on(":").join(formattedBytes);
}
public static Collection<String> getHostnames(X509Certificate certificate) throws CertificateParsingException {
Set<String> hostnames = new HashSet<>();
for (List<?> data : certificate.getSubjectAlternativeNames()) {
if (Objects.equals(data.get(0), 2) && data.get(1) instanceof String)
hostnames.add((String) data.get(1));
}
return hostnames;
}
}
package de.kuschku.util.certificates;
import android.content.Context;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import de.kuschku.libquassel.ssl.CertificateManager;
import de.kuschku.libquassel.ssl.UnknownCertificateException;
import de.kuschku.util.ServerAddress;
public class SQLiteCertificateManager implements CertificateManager {
private CertificateDatabaseHandler handler;
public SQLiteCertificateManager(Context context) {
this.handler = new CertificateDatabaseHandler(context);
}
@Override
public boolean isTrusted(X509Certificate certificate, ServerAddress core) {
try {
certificate.checkValidity();
return handler.findCertificates(core.host).contains(CertificateUtils.certificateToFingerprint(certificate));
} catch (Exception e) {
return false;
}
}
@Override
public boolean addCertificate(X509Certificate certificate, ServerAddress core) {
try {
return handler.addCertificate(CertificateUtils.certificateToFingerprint(certificate), core.host);
} catch (Exception e) {
return false;
}
}
@Override
public boolean removeCertificate(X509Certificate certificate, ServerAddress core) {
try {
return handler.removeCertificate(CertificateUtils.certificateToFingerprint(certificate), core.host);
} catch (Exception e) {
return false;
}
}
@Override
public boolean removeAllCertificates(ServerAddress core) {
return handler.removeCertificates(core.host);
}
@Override
public void checkTrusted(X509Certificate certificate, ServerAddress address) throws UnknownCertificateException {
if (!isTrusted(certificate, address))
throw new UnknownCertificateException(certificate, address);
}
public List<String> findCertificates(ServerAddress core) {
return handler.findCertificates(core.host);
}
public Map<String, Collection<String>> findAllCertificates() {
return handler.findAllCertificates();
}
}
......@@ -25,7 +25,6 @@ import java.util.regex.Pattern;
import de.kuschku.quasseldroid_ng.R;
import de.kuschku.quasseldroid_ng.ui.theme.AppContext;
import de.kuschku.quasseldroid_ng.ui.theme.ThemeUtil;
import de.kuschku.util.ui.MessageUtil;
public class IrcFormatHelper {
......
......@@ -14,10 +14,19 @@ import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.InterruptibleChannel;
import java.security.GeneralSecurityException;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import de.kuschku.libquassel.ssl.CertificateManager;
import de.kuschku.libquassel.ssl.QuasselTrustManager;
import de.kuschku.util.CompatibilityUtils;
import de.kuschku.util.ServerAddress;
public class WrappedChannel implements Flushable, ByteChannel, InterruptibleChannel {
@Nullable
......@@ -29,6 +38,9 @@ public class WrappedChannel implements Flushable, ByteChannel, InterruptibleChan
@Nullable
private DataOutputStream out;
@Nullable
private Socket socket = null;
private WrappedChannel(@Nullable InputStream in, @Nullable OutputStream out) {
this.rawIn = in;
this.rawOut = out;
......@@ -36,6 +48,11 @@ public class WrappedChannel implements Flushable, ByteChannel, InterruptibleChan
if (this.rawOut != null) this.out = new DataOutputStream(rawOut);
}
public WrappedChannel(Socket s) throws IOException {
this(s.getInputStream(), s.getOutputStream());
this.socket = s;
}
@NonNull
public static WrappedChannel ofStreams(@Nullable InputStream in, @Nullable OutputStream out) {
return new WrappedChannel(in, out);
......@@ -43,7 +60,7 @@ public class WrappedChannel implements Flushable, ByteChannel, InterruptibleChan
@NonNull
public static WrappedChannel ofSocket(@NonNull Socket s) throws IOException {
return new WrappedChannel(s.getInputStream(), s.getOutputStream());
return new WrappedChannel(s);
}
@Nullable
......@@ -54,6 +71,20 @@ public class WrappedChannel implements Flushable, ByteChannel, InterruptibleChan
);
}
public static WrappedChannel withSSL(@NonNull WrappedChannel channel,
@NonNull CertificateManager certificateManager,
@NonNull ServerAddress address) throws GeneralSecurityException, IOException {
SSLContext context = SSLContext.getInstance("TLSv1.2");
TrustManager[] managers = new TrustManager[]{QuasselTrustManager.fromDefault(certificateManager, address)};
context.init(null, managers, null);
SSLSocketFactory factory = context.getSocketFactory();
SSLSocket socket = (SSLSocket) factory.createSocket(channel.socket, address.host, address.port, true);
socket.setUseClientMode(true);
socket.startHandshake();
return WrappedChannel.ofSocket(socket);
}
/**
* Reads a sequence of bytes from this channel into the given buffer.
* <p>
......
......@@ -2,5 +2,6 @@ package de.kuschku.util.observables;
public interface ContentComparable<T extends ContentComparable<T>> extends Comparable<T> {
boolean areItemsTheSame(T other);
boolean areContentsTheSame(T other);
}
......@@ -3,7 +3,6 @@ package de.kuschku.util.observables.lists;
import android.support.annotation.NonNull;
import de.kuschku.util.observables.IObservable;
import de.kuschku.util.observables.callbacks.UICallback;
import de.kuschku.util.observables.callbacks.UIChildCallback;
import de.kuschku.util.observables.callbacks.UIChildParentCallback;
import de.kuschku.util.observables.callbacks.UIParentCallback;
......
......@@ -213,6 +213,11 @@ public class ObservableSortedList<T> implements IObservableList<UICallback, T> {
callback.notifyItemChanged(position);
}
@Override
public String toString() {
return Arrays.toString(toArray());
}
public interface ItemComparator<T> {
int compare(T o1, T o2);
......@@ -221,11 +226,6 @@ public class ObservableSortedList<T> implements IObservableList<UICallback, T> {
boolean areItemsTheSame(T item1, T item2);
}
@Override
public String toString() {
return Arrays.toString(toArray());
}
class Callback extends SortedList.Callback<T> {
@Override
public int compare(T o1, T o2) {
......
......@@ -17,42 +17,42 @@ public class DateTimeFormatHelper {
}
@NonNull
public DateTimeFormatter getTimeFormatter() {
return getTimeFormatter(context);
public static DateTimeFormatter getTimeFormatter(Context ctx) {
return DateTimeFormat.forPattern(((SimpleDateFormat) android.text.format.DateFormat.getTimeFormat(ctx)).toLocalizedPattern());
}
@NonNull
public DateTimeFormatter getDateFormatter() {
return getDateFormatter(context);
public static DateTimeFormatter getDateFormatter(Context ctx) {
return DateTimeFormat.forPattern(((SimpleDateFormat) android.text.format.DateFormat.getDateFormat(ctx)).toLocalizedPattern());
}
@NonNull
public DateTimeFormatter getLongDateFormatter() {
return getLongDateFormatter(context);
public static DateTimeFormatter getLongDateFormatter(Context ctx) {
return DateTimeFormat.forPattern(((SimpleDateFormat) android.text.format.DateFormat.getLongDateFormat(ctx)).toLocalizedPattern());
}
@NonNull
public DateTimeFormatter getMediumDateFormatter() {
return getMediumDateFormatter(context);
public static DateTimeFormatter getMediumDateFormatter(Context ctx) {
return DateTimeFormat.forPattern(((SimpleDateFormat) android.text.format.DateFormat.getMediumDateFormat(ctx)).toLocalizedPattern());
}
@NonNull
public static DateTimeFormatter getTimeFormatter(Context ctx) {
return DateTimeFormat.forPattern(((SimpleDateFormat) android.text.format.DateFormat.getTimeFormat(ctx)).toLocalizedPattern());
public DateTimeFormatter getTimeFormatter() {
return getTimeFormatter(context);
}
@NonNull
public static DateTimeFormatter getDateFormatter(Context ctx) {
return DateTimeFormat.forPattern(((SimpleDateFormat) android.text.format.DateFormat.getDateFormat(ctx)).toLocalizedPattern());
public DateTimeFormatter getDateFormatter() {
return getDateFormatter(context);
}
@NonNull
public static DateTimeFormatter getLongDateFormatter(Context ctx) {
return DateTimeFormat.forPattern(((SimpleDateFormat) android.text.format.DateFormat.getLongDateFormat(ctx)).toLocalizedPattern());
public DateTimeFormatter getLongDateFormatter() {
return getLongDateFormatter(context);
}
@NonNull
public static DateTimeFormatter getMediumDateFormatter(Context ctx) {
return DateTimeFormat.forPattern(((SimpleDateFormat) android.text.format.DateFormat.getMediumDateFormat(ctx)).toLocalizedPattern());
public DateTimeFormatter getMediumDateFormatter() {
return getMediumDateFormatter(context);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment