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