diff --git a/application/res/layout/serveradd.xml b/application/res/layout/serveradd.xml index 48454fda..532513f5 100644 --- a/application/res/layout/serveradd.xml +++ b/application/res/layout/serveradd.xml @@ -89,12 +89,29 @@ along with Yaaic. If not, see . android:layout_height="wrap_content" android:text="@string/server_port" android:visibility="gone" /> - + + android:entries="@array/server_securityTypes" /> + + 20 30 + + None + SSL/TLS (Accept all certificates) + SSL/TLS + SSL/TLS (Fixed key) + diff --git a/application/res/values/strings.xml b/application/res/values/strings.xml index 4e7fbdb7..88995e25 100644 --- a/application/res/values/strings.xml +++ b/application/res/values/strings.xml @@ -95,7 +95,8 @@ Line is missing Nickname %1$s already in use Could not log into the IRC server %1$s:%2$d - Could not connect to %1$s:%2$d + Could not connect to %1$s:%2$d: %3$s + Could not handshake with %1$s:%2$d: %3$s Send a message to all channels Sets you away @@ -234,4 +235,8 @@ Use fullscreen keyboard when in landscape mode History size Number of lines of conversation history to keep + + Security type + SHA-1 fingerprint + Invalid fingerprint value diff --git a/application/src/org/jibble/pircbot/PircBot.java b/application/src/org/jibble/pircbot/PircBot.java index 2f355ff7..da5a2a33 100644 --- a/application/src/org/jibble/pircbot/PircBot.java +++ b/application/src/org/jibble/pircbot/PircBot.java @@ -37,8 +37,11 @@ General Public License (GPL) and the www.jibble.org Commercial License. import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; +import java.security.NoSuchAlgorithmException; +import java.security.KeyManagementException; import org.yaaic.ssl.NaiveTrustManager; +import org.yaaic.ssl.FixedTrustManager; import org.yaaic.tools.Base64; /** @@ -88,6 +91,8 @@ public abstract class PircBot implements ReplyConstants { private static final int VOICE_ADD = 3; private static final int VOICE_REMOVE = 4; + public enum SecurityType { NONE, TLS_INSECURE, TLS, TLS_FIXED_KEY }; + /** * Constructs a PircBot with the default settings. Your own constructors * in classes which extend the PircBot abstract class should be responsible @@ -105,7 +110,10 @@ public PircBot() {} * @throws IrcException if the server would not let us join it. * @throws NickAlreadyInUseException if our nick is already in use on the server. */ - public final synchronized void connect(String hostname) throws IOException, IrcException, NickAlreadyInUseException { + public final synchronized void connect(String hostname) + throws IOException, IrcException, NickAlreadyInUseException, + NoSuchAlgorithmException, KeyManagementException + { this.connect(hostname, 6667, null); } @@ -121,7 +129,10 @@ public final synchronized void connect(String hostname) throws IOException, IrcE * @throws IrcException if the server would not let us join it. * @throws NickAlreadyInUseException if our nick is already in use on the server. */ - public final synchronized void connect(String hostname, int port) throws IOException, IrcException, NickAlreadyInUseException { + public final synchronized void connect(String hostname, int port) + throws IOException, IrcException, NickAlreadyInUseException, + NoSuchAlgorithmException, KeyManagementException + { this.connect(hostname, port, null); } @@ -139,7 +150,7 @@ public final synchronized void connect(String hostname, int port) throws IOExcep * @throws IrcException if the server would not let us join it. * @throws NickAlreadyInUseException if our nick is already in use on the server. */ - public final synchronized void connect(String hostname, int port, String password) throws IOException, IrcException, NickAlreadyInUseException { + public final synchronized void connect(String hostname, int port, String password) throws IOException, IrcException, NickAlreadyInUseException, NoSuchAlgorithmException, KeyManagementException { _registered = false; _server = hostname; @@ -159,20 +170,24 @@ public final synchronized void connect(String hostname, int port, String passwor // Connect to the server. // XXX: PircBot Patch for SSL - if (_useSSL) { - try { - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, new X509TrustManager[] { new NaiveTrustManager() }, null); - SSLSocketFactory factory = context.getSocketFactory(); - SSLSocket ssocket = (SSLSocket) factory.createSocket(hostname, port); - ssocket.startHandshake(); - _socket = ssocket; - } - catch(Exception e) - { - // XXX: It's not really an IOException :) - throw new IOException("Cannot open SSL socket"); + if (_securityType != SecurityType.NONE){ + SSLContext context = SSLContext.getInstance("TLS"); + switch (_securityType){ + case TLS_INSECURE: + context.init(null, new X509TrustManager[] { new NaiveTrustManager() }, null); + break; + case TLS: + /* Use default certificate validation. */ + context.init(null, null, null); + break; + case TLS_FIXED_KEY: + context.init(null, new X509TrustManager[] { new FixedTrustManager(_fingerprint) }, null); + break; } + SSLSocketFactory factory = context.getSocketFactory(); + SSLSocket ssocket = (SSLSocket) factory.createSocket(hostname, port); + ssocket.startHandshake(); + _socket = ssocket; } else { _socket = new Socket(hostname, port); } @@ -278,21 +293,24 @@ public final synchronized void connect(String hostname, int port, String passwor * @throws IrcException if the server would not let us join it. * @throws NickAlreadyInUseException if our nick is already in use on the server. */ - public final synchronized void reconnect() throws IOException, IrcException, NickAlreadyInUseException{ + public final synchronized void reconnect() + throws IOException, IrcException, NickAlreadyInUseException, + NoSuchAlgorithmException, KeyManagementException + { if (getServer() == null) { throw new IrcException("Cannot reconnect to an IRC server because we were never connected to one previously!"); } connect(getServer(), getPort(), getPassword()); } - /** - * Set wether SSL should be used to connect to the server. - * - * @author Sebastian Kaspari - */ - public void setUseSSL(boolean useSSL) + public void setSecurityType(SecurityType securityType) + { + _securityType = securityType; + } + + public void setFingerprint(String fingerprint) { - _useSSL = useSSL; + _fingerprint = fingerprint; } /** @@ -3171,7 +3189,8 @@ else if (userMode == VOICE_REMOVE) { // Default settings for the PircBot. private boolean _autoNickChange = false; private int _autoNickTries = 1; - private boolean _useSSL = false; + private SecurityType _securityType = SecurityType.NONE; + private String _fingerprint = ""; private boolean _registered = false; private String _name = "PircBot"; diff --git a/application/src/org/yaaic/activity/AddServerActivity.java b/application/src/org/yaaic/activity/AddServerActivity.java index 5a2e289f..5ae837a3 100644 --- a/application/src/org/yaaic/activity/AddServerActivity.java +++ b/application/src/org/yaaic/activity/AddServerActivity.java @@ -33,6 +33,8 @@ import org.yaaic.model.Identity; import org.yaaic.model.Server; import org.yaaic.model.Status; +import org.yaaic.ssl.FixedTrustManager; +import org.jibble.pircbot.PircBot; import android.content.Intent; import android.net.Uri; @@ -44,6 +46,8 @@ import android.widget.CheckBox; import android.widget.EditText; import android.widget.Spinner; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; import android.widget.Toast; import com.actionbarsherlock.app.ActionBar; @@ -57,7 +61,8 @@ * * @author Sebastian Kaspari */ -public class AddServerActivity extends SherlockActivity implements OnClickListener +public class AddServerActivity extends SherlockActivity + implements OnClickListener, OnItemSelectedListener { private static final int REQUEST_CODE_CHANNELS = 1; private static final int REQUEST_CODE_COMMANDS = 2; @@ -94,6 +99,7 @@ public void onCreate(Bundle savedInstanceState) ((Button) findViewById(R.id.channels)).setOnClickListener(this); ((Button) findViewById(R.id.commands)).setOnClickListener(this); ((Button) findViewById(R.id.authentication)).setOnClickListener(this); + ((Spinner) findViewById(R.id.securityType)).setOnItemSelectedListener(this); Spinner spinner = (Spinner) findViewById(R.id.charset); String[] charsets = getResources().getStringArray(R.array.charsets); @@ -123,7 +129,8 @@ public void onCreate(Bundle savedInstanceState) ((EditText) findViewById(R.id.nickname)).setText(server.getIdentity().getNickname()); ((EditText) findViewById(R.id.ident)).setText(server.getIdentity().getIdent()); ((EditText) findViewById(R.id.realname)).setText(server.getIdentity().getRealName()); - ((CheckBox) findViewById(R.id.useSSL)).setChecked(server.useSSL()); + ((Spinner) findViewById(R.id.securityType)).setSelection(server.securityType().ordinal()); + ((EditText) findViewById(R.id.fingerprint)).setText(server.fingerprint()); // Select charset if (server.getCharset() != null) { @@ -157,6 +164,8 @@ public void onCreate(Bundle savedInstanceState) ((EditText) findViewById(R.id.password)).setText(String.valueOf(uri.getQuery())); } } + + showHide(); } /** @@ -224,6 +233,37 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) } } + private void showHide() + { + Spinner v_securityType = (Spinner) findViewById(R.id.securityType); + if (v_securityType.getSelectedItemPosition() >= 0){ + PircBot.SecurityType securityType = PircBot.SecurityType.values()[v_securityType.getSelectedItemPosition()]; + if (securityType == PircBot.SecurityType.TLS_FIXED_KEY){ + findViewById(R.id.fingerprint_label).setVisibility(View.VISIBLE); + findViewById(R.id.fingerprint).setVisibility(View.VISIBLE); + } else { + findViewById(R.id.fingerprint_label).setVisibility(View.GONE); + findViewById(R.id.fingerprint).setVisibility(View.GONE); + } + } + } + + @Override + public void onItemSelected(AdapterView parent, View v, int position, long id) + { + switch (parent.getId()) { + case R.id.securityType: + showHide(); + break; + } + } + + @Override + public void onNothingSelected(AdapterView parent) + { + + } + /** * On click add server or cancel activity */ @@ -368,7 +408,12 @@ private Server getServerFromView() int port = Integer.parseInt(((EditText) findViewById(R.id.port)).getText().toString().trim()); String password = ((EditText) findViewById(R.id.password)).getText().toString().trim(); String charset = ((Spinner) findViewById(R.id.charset)).getSelectedItem().toString(); - Boolean useSSL = ((CheckBox) findViewById(R.id.useSSL)).isChecked(); + PircBot.SecurityType securityType = PircBot.SecurityType.values()[ + ((Spinner) findViewById(R.id.securityType)).getSelectedItemPosition()]; + String fingerprint = ""; + if (securityType == PircBot.SecurityType.TLS_FIXED_KEY){ + fingerprint = ((EditText) findViewById(R.id.fingerprint)).getText().toString().trim(); + } // not in use yet //boolean autoConnect = ((CheckBox) findViewById(R.id.autoconnect)).isChecked(); @@ -379,7 +424,8 @@ private Server getServerFromView() server.setPassword(password); server.setTitle(title); server.setCharset(charset); - server.setUseSSL(useSSL); + server.setSecurityType(securityType); + server.setFingerprint(fingerprint); server.setStatus(Status.DISCONNECTED); return server; @@ -433,6 +479,15 @@ private void validateServer() throws ValidationException throw new ValidationException(getResources().getString(R.string.validation_invalid_port)); } + PircBot.SecurityType securityType = PircBot.SecurityType.values() + [((Spinner) findViewById(R.id.securityType)).getSelectedItemPosition()]; + if (securityType == PircBot.SecurityType.TLS_FIXED_KEY){ + String fingerprint = ((EditText) findViewById(R.id.fingerprint)).getText().toString().trim(); + if (FixedTrustManager.parseDigest(fingerprint) == null){ + throw new ValidationException(getResources().getString(R.string.validation_bad_fingerprint)); + } + } + try { "".getBytes(charset); } diff --git a/application/src/org/yaaic/db/Database.java b/application/src/org/yaaic/db/Database.java index 80fb5d14..77fc8ff3 100644 --- a/application/src/org/yaaic/db/Database.java +++ b/application/src/org/yaaic/db/Database.java @@ -28,6 +28,7 @@ import org.yaaic.model.Identity; import org.yaaic.model.Server; import org.yaaic.model.Status; +import org.jibble.pircbot.PircBot; import android.content.ContentValues; import android.content.Context; @@ -44,7 +45,7 @@ public class Database extends SQLiteOpenHelper { private static final String DATABASE_NAME = "servers.db"; - private static final int DATABASE_VERSION = 5; + private static final int DATABASE_VERSION = 6; /** * Create a new helper for database access @@ -69,7 +70,8 @@ public void onCreate(SQLiteDatabase db) + ServerConstants.PORT + " INTEGER, " + ServerConstants.PASSWORD + " TEXT, " + ServerConstants.AUTOCONNECT + " BOOLEAN, " - + ServerConstants.USE_SSL + " BOOLEAN, " + + ServerConstants.SECURITY_TYPE + " INTEGER, " + + ServerConstants.FINGERPRINT + " TEXT, " + ServerConstants.CHARSET + " TEXT, " + ServerConstants.IDENTITY + " INTEGER, " + ServerConstants.NICKSERV_PASSWORD + " TEXT, " @@ -158,6 +160,13 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) db.execSQL("ALTER TABLE " + ServerConstants.TABLE_NAME + " ADD " + ServerConstants.NICKSERV_PASSWORD + " TEXT AFTER " + ServerConstants.CHARSET + ";"); db.execSQL("ALTER TABLE " + ServerConstants.TABLE_NAME + " ADD " + ServerConstants.SASL_USERNAME + " TEXT AFTER " + ServerConstants.NICKSERV_PASSWORD + ";"); db.execSQL("ALTER TABLE " + ServerConstants.TABLE_NAME + " ADD " + ServerConstants.SASL_PASSWORD + " TEXT AFTER " + ServerConstants.SASL_USERNAME + ";"); + oldVersion = 5; + } + + if (oldVersion == 5) { + db.execSQL("ALTER TABLE " + ServerConstants.TABLE_NAME + " ADD " + ServerConstants.SECURITY_TYPE + " INTEGER AFTER " + ServerConstants.USE_SSL + ";"); + db.execSQL("ALTER TABLE " + ServerConstants.TABLE_NAME + " ADD " + ServerConstants.FINGERPRINT + " TEXT AFTER " + ServerConstants.SECURITY_TYPE + ";"); + oldVersion = 6; } } @@ -176,7 +185,8 @@ public long addServer(Server server, int identityId) values.put(ServerConstants.PORT, server.getPort()); values.put(ServerConstants.PASSWORD, server.getPassword()); values.put(ServerConstants.AUTOCONNECT, false); - values.put(ServerConstants.USE_SSL, server.useSSL()); + values.put(ServerConstants.SECURITY_TYPE, server.securityType().ordinal()); + values.put(ServerConstants.FINGERPRINT, server.fingerprint()); values.put(ServerConstants.IDENTITY, identityId); values.put(ServerConstants.CHARSET, server.getCharset()); @@ -204,7 +214,8 @@ public void updateServer(int serverId, Server server, int identityId) values.put(ServerConstants.PORT, server.getPort()); values.put(ServerConstants.PASSWORD, server.getPassword()); values.put(ServerConstants.AUTOCONNECT, false); - values.put(ServerConstants.USE_SSL, server.useSSL()); + values.put(ServerConstants.SECURITY_TYPE, server.securityType().ordinal()); + values.put(ServerConstants.FINGERPRINT, server.fingerprint()); values.put(ServerConstants.IDENTITY, identityId); values.put(ServerConstants.CHARSET, server.getCharset()); @@ -423,11 +434,8 @@ private Server populateServer(Cursor cursor) server.setPassword(cursor.getString(cursor.getColumnIndex(ServerConstants.PASSWORD))); server.setId(cursor.getInt(cursor.getColumnIndex((ServerConstants._ID)))); server.setCharset(cursor.getString(cursor.getColumnIndex(ServerConstants.CHARSET))); - - String useSSLvalue = cursor.getString(cursor.getColumnIndex(ServerConstants.USE_SSL)); - if (useSSLvalue != null && useSSLvalue.equals("1")) { - server.setUseSSL(true); - } + server.setSecurityType(PircBot.SecurityType.values()[cursor.getInt(cursor.getColumnIndex(ServerConstants.SECURITY_TYPE))]); + server.setFingerprint(cursor.getString(cursor.getColumnIndex(ServerConstants.FINGERPRINT))); server.setStatus(Status.DISCONNECTED); diff --git a/application/src/org/yaaic/db/ServerConstants.java b/application/src/org/yaaic/db/ServerConstants.java index 267aeefe..022b340f 100644 --- a/application/src/org/yaaic/db/ServerConstants.java +++ b/application/src/org/yaaic/db/ServerConstants.java @@ -38,6 +38,8 @@ public interface ServerConstants extends BaseColumns public static final String PASSWORD = "password"; public static final String AUTOCONNECT = "autoConnect"; public static final String USE_SSL = "useSSL"; + public static final String SECURITY_TYPE = "securityType"; + public static final String FINGERPRINT = "fingerprint"; public static final String CHARSET = "charset"; public static final String IDENTITY = "identity"; public static final String NICKSERV_PASSWORD = "nickserv_password"; @@ -54,7 +56,8 @@ public interface ServerConstants extends BaseColumns PORT, PASSWORD, AUTOCONNECT, - USE_SSL, + SECURITY_TYPE, + FINGERPRINT, CHARSET, IDENTITY, NICKSERV_PASSWORD, diff --git a/application/src/org/yaaic/irc/IRCService.java b/application/src/org/yaaic/irc/IRCService.java index 75fbe60a..ed4380c0 100644 --- a/application/src/org/yaaic/irc/IRCService.java +++ b/application/src/org/yaaic/irc/IRCService.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; +import java.security.cert.CertPathValidatorException; import org.jibble.pircbot.IrcException; import org.jibble.pircbot.NickAlreadyInUseException; @@ -460,7 +461,8 @@ public void run() { connection.setAliases(server.getIdentity().getAliases()); connection.setIdent(server.getIdentity().getIdent()); connection.setRealName(server.getIdentity().getRealName()); - connection.setUseSSL(server.useSSL()); + connection.setSecurityType(server.securityType()); + connection.setFingerprint(server.fingerprint()); if (server.getCharset() != null) { connection.setEncoding(server.getCharset()); @@ -495,8 +497,13 @@ public void run() { } else if (e instanceof IrcException) { message = new Message(getString(R.string.irc_login_error, server.getHost(), server.getPort())); server.setMayReconnect(false); + } else if (e instanceof CertPathValidatorException) { + message = new Message(getString(R.string.ssl_handshake_error, + server.getHost(), server.getPort(), e.getLocalizedMessage())); + server.setMayReconnect(false); } else { - message = new Message(getString(R.string.could_not_connect, server.getHost(), server.getPort())); + message = new Message(getString(R.string.could_not_connect, + server.getHost(), server.getPort(), e.getLocalizedMessage())); if (settings.isReconnectEnabled()) { Intent rIntent = new Intent(Broadcast.SERVER_RECONNECT + serverId); PendingIntent pendingRIntent = PendingIntent.getBroadcast(service, 0, rIntent, 0); diff --git a/application/src/org/yaaic/model/Server.java b/application/src/org/yaaic/model/Server.java index fbe73ba2..7fd78c2e 100644 --- a/application/src/org/yaaic/model/Server.java +++ b/application/src/org/yaaic/model/Server.java @@ -25,6 +25,7 @@ import java.util.LinkedHashMap; import org.yaaic.R; +import org.jibble.pircbot.PircBot; /** * A server as we know it @@ -39,7 +40,8 @@ public class Server private int port; private String password; private String charset; - private boolean useSSL = false; + private PircBot.SecurityType securityType = PircBot.SecurityType.NONE; + private String fingerprint = null; private Identity identity; private Authentication authentication; @@ -222,22 +224,24 @@ public String getCharset() return charset; } - /** - * Set if this connections needs to use ssl - */ - public void setUseSSL(boolean useSSL) + public void setSecurityType(PircBot.SecurityType securityType) { - this.useSSL = useSSL; + this.securityType = securityType; } - /** - * Does this connection use SSL? - * - * @return true if SSL should be used, false otherwise - */ - public boolean useSSL() + public PircBot.SecurityType securityType() + { + return securityType; + } + + public void setFingerprint(String fingerprint) + { + this.fingerprint = fingerprint; + } + + public String fingerprint() { - return useSSL; + return fingerprint; } /** diff --git a/application/src/org/yaaic/ssl/FixedTrustManager.java b/application/src/org/yaaic/ssl/FixedTrustManager.java new file mode 100644 index 00000000..bfffaf25 --- /dev/null +++ b/application/src/org/yaaic/ssl/FixedTrustManager.java @@ -0,0 +1,122 @@ +/* +Yaaic - Yet Another Android IRC Client + +Copyright 2009-2013 Sebastian Kaspari +Copyright 2013 Joshua Phillips + +This file is part of Yaaic. + +Yaaic 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. + +Yaaic 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 Yaaic. If not, see . + */ +package org.yaaic.ssl; + +import java.security.cert.CertificateException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.security.MessageDigest; +import java.io.ByteArrayOutputStream; +import java.util.Arrays; + +import javax.net.ssl.X509TrustManager; + +/** + * Trust manager that accepts only one fingerprint + * + * @author Joshua Phillips + */ + + /* TODO TODO TODO */ +public class FixedTrustManager implements X509TrustManager +{ + + private byte[] _fingerprint = null; + private static final String hexChars = "0123456789ABCDEF"; + + public static byte[] parseDigest(String digest) + { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + int nextByte = 0; + int nibbleIndex = 0; + for (int i=0; i