Introduce XmppConnection v3
The various layers of the app are too intertwined to refactor them in place. The C3 refactor is going to create a parallel architecture for all classes that have too strong of a connection to other parts of the app. This commit introduces XmppConnection v3 that keeps a lot of the logic of the privous XmppConnection but cuts ties to XmppConnectionService and the very stateful `entites.Account`. The latter is replaced by a lightweight immutable account model. The reconnection logic has been kept but was moved from XmppConnectionService to a singleton ConnectionPool.
This commit is contained in:
parent
94dde9f433
commit
7ee3e07946
|
@ -53,6 +53,8 @@ dependencies {
|
||||||
annotationProcessor "androidx.room:room-compiler:$room_version"
|
annotationProcessor "androidx.room:room-compiler:$room_version"
|
||||||
implementation "androidx.room:room-guava:$room_version"
|
implementation "androidx.room:room-guava:$room_version"
|
||||||
|
|
||||||
|
implementation "androidx.security:security-crypto:1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// legacy dependencies. Ideally everything below should be carefully reviewed and eventually moved up
|
// legacy dependencies. Ideally everything below should be carefully reviewed and eventually moved up
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "c78cb993428558b863fd91c46b608926",
|
"identityHash": "4a70ff0733436f5a2a08e7abb8e6cc95",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "account",
|
"tableName": "account",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `address` TEXT NOT NULL, `resource` TEXT, `randomSeed` BLOB, `enabled` INTEGER NOT NULL, `rosterVersion` TEXT, `hostname` TEXT, `port` INTEGER, `directTls` INTEGER, `proxytype` TEXT, `proxyhostname` TEXT, `proxyport` INTEGER)",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `address` TEXT NOT NULL, `resource` TEXT, `randomSeed` BLOB, `enabled` INTEGER NOT NULL, `quickStartAvailable` INTEGER NOT NULL, `pendingRegistration` INTEGER NOT NULL, `loggedInSuccessfully` INTEGER NOT NULL, `showErrorNotification` INTEGER NOT NULL, `rosterVersion` TEXT, `hostname` TEXT, `port` INTEGER, `directTls` INTEGER, `proxytype` TEXT, `proxyhostname` TEXT, `proxyport` INTEGER)",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
|
@ -38,6 +38,30 @@
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "quickStartAvailable",
|
||||||
|
"columnName": "quickStartAvailable",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pendingRegistration",
|
||||||
|
"columnName": "pendingRegistration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "loggedInSuccessfully",
|
||||||
|
"columnName": "loggedInSuccessfully",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "showErrorNotification",
|
||||||
|
"columnName": "showErrorNotification",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "rosterVersion",
|
"fieldPath": "rosterVersion",
|
||||||
"columnName": "rosterVersion",
|
"columnName": "rosterVersion",
|
||||||
|
@ -830,7 +854,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "presence",
|
"tableName": "presence",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `resource` TEXT, `type` TEXT, `show` TEXT, `status` TEXT, `vCardPhoto` TEXT, `occupantId` TEXT, `mucUserAffiliation` TEXT, `mucUserRole` TEXT, `mucUserJid` TEXT, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `resource` TEXT, `type` TEXT, `show` TEXT, `status` TEXT, `vCardPhoto` TEXT, `occupantId` TEXT, `mucUserAffiliation` TEXT, `mucUserRole` TEXT, `mucUserJid` TEXT, `mucUserSelf` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
|
@ -903,6 +927,12 @@
|
||||||
"columnName": "mucUserJid",
|
"columnName": "mucUserJid",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": false
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mucUserSelf",
|
||||||
|
"columnName": "mucUserSelf",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
|
@ -1159,7 +1189,7 @@
|
||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c78cb993428558b863fd91c46b608926')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4a70ff0733436f5a2a08e7abb8e6cc95')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -74,6 +74,7 @@
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name="im.conversations.android.Conversations"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:appCategory="social"
|
android:appCategory="social"
|
||||||
android:fullBackupContent="@xml/backup_content"
|
android:fullBackupContent="@xml/backup_content"
|
||||||
|
|
|
@ -38,18 +38,18 @@ import android.preference.PreferenceManager;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.io.ByteStreams;
|
import com.google.common.io.ByteStreams;
|
||||||
import com.google.common.io.CharStreams;
|
import com.google.common.io.CharStreams;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
import org.json.JSONArray;
|
import eu.siacs.conversations.R;
|
||||||
import org.json.JSONException;
|
import eu.siacs.conversations.crypto.XmppDomainVerifier;
|
||||||
import org.json.JSONObject;
|
import eu.siacs.conversations.entities.MTMDecision;
|
||||||
|
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||||
|
import eu.siacs.conversations.persistance.FileBackend;
|
||||||
|
import eu.siacs.conversations.ui.MemorizingActivity;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
@ -73,44 +73,48 @@ import java.util.Locale;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.net.ssl.TrustManager;
|
import javax.net.ssl.TrustManager;
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
import javax.net.ssl.X509TrustManager;
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
import org.json.JSONArray;
|
||||||
import eu.siacs.conversations.Config;
|
import org.json.JSONException;
|
||||||
import eu.siacs.conversations.R;
|
import org.json.JSONObject;
|
||||||
import eu.siacs.conversations.crypto.XmppDomainVerifier;
|
|
||||||
import eu.siacs.conversations.entities.MTMDecision;
|
|
||||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
|
||||||
import eu.siacs.conversations.persistance.FileBackend;
|
|
||||||
import eu.siacs.conversations.ui.MemorizingActivity;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A X509 trust manager implementation which asks the user about invalid
|
* A X509 trust manager implementation which asks the user about invalid certificates and memorizes
|
||||||
* certificates and memorizes their decision.
|
* their decision.
|
||||||
* <p>
|
*
|
||||||
* The certificate validity is checked using the system default X509
|
* <p>The certificate validity is checked using the system default X509 TrustManager, creating a
|
||||||
* TrustManager, creating a query Dialog if the check fails.
|
* query Dialog if the check fails.
|
||||||
* <p>
|
*
|
||||||
* <b>WARNING:</b> This only works if a dedicated thread is used for
|
* <p><b>WARNING:</b> This only works if a dedicated thread is used for opening sockets!
|
||||||
* opening sockets!
|
|
||||||
*/
|
*/
|
||||||
public class MemorizingTrustManager {
|
public class MemorizingTrustManager {
|
||||||
|
|
||||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
|
private static final SimpleDateFormat DATE_FORMAT =
|
||||||
|
new SimpleDateFormat("yyyy-MM-dd", Locale.US);
|
||||||
|
|
||||||
final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
|
static final String DECISION_INTENT = "de.duenndns.ssl.DECISION";
|
||||||
public final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
|
public static final String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
|
||||||
public final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
|
public static final String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
|
||||||
public final static String DECISION_TITLE_ID = DECISION_INTENT + ".titleId";
|
public static final String DECISION_TITLE_ID = DECISION_INTENT + ".titleId";
|
||||||
final static String NO_TRUST_ANCHOR = "Trust anchor for certification path not found.";
|
static final String NO_TRUST_ANCHOR = "Trust anchor for certification path not found.";
|
||||||
private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
private static final Pattern PATTERN_IPV4 =
|
||||||
private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
Pattern.compile(
|
||||||
private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
"\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
||||||
private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
|
private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED =
|
||||||
private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
|
Pattern.compile(
|
||||||
private final static Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
|
"\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)"
|
||||||
|
+ " ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
||||||
|
private static final Pattern PATTERN_IPV6_6HEX4DEC =
|
||||||
|
Pattern.compile(
|
||||||
|
"\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
||||||
|
private static final Pattern PATTERN_IPV6_HEXCOMPRESSED =
|
||||||
|
Pattern.compile(
|
||||||
|
"\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
|
||||||
|
private static final Pattern PATTERN_IPV6 =
|
||||||
|
Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(MemorizingTrustManager.class.getName());
|
||||||
static String KEYSTORE_DIR = "KeyStore";
|
static String KEYSTORE_DIR = "KeyStore";
|
||||||
static String KEYSTORE_FILE = "KeyStore.bks";
|
static String KEYSTORE_FILE = "KeyStore.bks";
|
||||||
private static int decisionId = 0;
|
private static int decisionId = 0;
|
||||||
|
@ -125,19 +129,32 @@ public class MemorizingTrustManager {
|
||||||
private X509TrustManager appTrustManager;
|
private X509TrustManager appTrustManager;
|
||||||
private String poshCacheDir;
|
private String poshCacheDir;
|
||||||
|
|
||||||
|
public static MemorizingTrustManager create(final Context context) {
|
||||||
|
final SharedPreferences preferences =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
|
||||||
|
final boolean dontTrustSystemCAs =
|
||||||
|
preferences.getBoolean(
|
||||||
|
"dont_trust_system_cas",
|
||||||
|
context.getResources().getBoolean(R.bool.dont_trust_system_cas));
|
||||||
|
if (dontTrustSystemCAs) {
|
||||||
|
return new MemorizingTrustManager(context.getApplicationContext(), null);
|
||||||
|
} else {
|
||||||
|
return new MemorizingTrustManager(context.getApplicationContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager.
|
* Creates an instance of the MemorizingTrustManager class that falls back to a custom
|
||||||
* <p>
|
* TrustManager.
|
||||||
* You need to supply the application context. This has to be one of:
|
*
|
||||||
* - Application
|
* <p>You need to supply the application context. This has to be one of: - Application -
|
||||||
* - Activity
|
* Activity - Service
|
||||||
* - Service
|
*
|
||||||
* <p>
|
* <p>The context is used for file management, to display the dialog / notification and for
|
||||||
* The context is used for file management, to display the dialog /
|
* obtaining translated strings.
|
||||||
* notification and for obtaining translated strings.
|
|
||||||
*
|
*
|
||||||
* @param m Context for the application.
|
* @param m Context for the application.
|
||||||
* @param defaultTrustManager Delegate trust management to this TM. If null, the user must accept every certificate.
|
* @param defaultTrustManager Delegate trust management to this TM. If null, the user must
|
||||||
|
* accept every certificate.
|
||||||
*/
|
*/
|
||||||
public MemorizingTrustManager(Context m, X509TrustManager defaultTrustManager) {
|
public MemorizingTrustManager(Context m, X509TrustManager defaultTrustManager) {
|
||||||
init(m);
|
init(m);
|
||||||
|
@ -147,14 +164,12 @@ public class MemorizingTrustManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance of the MemorizingTrustManager class using the system X509TrustManager.
|
* Creates an instance of the MemorizingTrustManager class using the system X509TrustManager.
|
||||||
* <p>
|
*
|
||||||
* You need to supply the application context. This has to be one of:
|
* <p>You need to supply the application context. This has to be one of: - Application -
|
||||||
* - Application
|
* Activity - Service
|
||||||
* - Activity
|
*
|
||||||
* - Service
|
* <p>The context is used for file management, to display the dialog / notification and for
|
||||||
* <p>
|
* obtaining translated strings.
|
||||||
* The context is used for file management, to display the dialog /
|
|
||||||
* notification and for obtaining translated strings.
|
|
||||||
*
|
*
|
||||||
* @param m Context for the application.
|
* @param m Context for the application.
|
||||||
*/
|
*/
|
||||||
|
@ -165,15 +180,16 @@ public class MemorizingTrustManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isIp(final String server) {
|
private static boolean isIp(final String server) {
|
||||||
return server != null && (
|
return server != null
|
||||||
PATTERN_IPV4.matcher(server).matches()
|
&& (PATTERN_IPV4.matcher(server).matches()
|
||||||
|| PATTERN_IPV6.matcher(server).matches()
|
|| PATTERN_IPV6.matcher(server).matches()
|
||||||
|| PATTERN_IPV6_6HEX4DEC.matcher(server).matches()
|
|| PATTERN_IPV6_6HEX4DEC.matcher(server).matches()
|
||||||
|| PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches()
|
|| PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches()
|
||||||
|| PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches());
|
|| PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getBase64Hash(X509Certificate certificate, String digest) throws CertificateEncodingException {
|
private static String getBase64Hash(X509Certificate certificate, String digest)
|
||||||
|
throws CertificateEncodingException {
|
||||||
MessageDigest md;
|
MessageDigest md;
|
||||||
try {
|
try {
|
||||||
md = MessageDigest.getInstance(digest);
|
md = MessageDigest.getInstance(digest);
|
||||||
|
@ -188,8 +204,7 @@ public class MemorizingTrustManager {
|
||||||
StringBuffer si = new StringBuffer();
|
StringBuffer si = new StringBuffer();
|
||||||
for (int i = 0; i < data.length; i++) {
|
for (int i = 0; i < data.length; i++) {
|
||||||
si.append(String.format("%02x", data[i]));
|
si.append(String.format("%02x", data[i]));
|
||||||
if (i < data.length - 1)
|
if (i < data.length - 1) si.append(":");
|
||||||
si.append(":");
|
|
||||||
}
|
}
|
||||||
return si.toString();
|
return si.toString();
|
||||||
}
|
}
|
||||||
|
@ -223,7 +238,8 @@ public class MemorizingTrustManager {
|
||||||
void init(final Context m) {
|
void init(final Context m) {
|
||||||
master = m;
|
master = m;
|
||||||
masterHandler = new Handler(m.getMainLooper());
|
masterHandler = new Handler(m.getMainLooper());
|
||||||
notificationManager = (NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE);
|
notificationManager =
|
||||||
|
(NotificationManager) master.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
Application app;
|
Application app;
|
||||||
if (m instanceof Application) {
|
if (m instanceof Application) {
|
||||||
|
@ -233,7 +249,8 @@ public class MemorizingTrustManager {
|
||||||
} else if (m instanceof AppCompatActivity) {
|
} else if (m instanceof AppCompatActivity) {
|
||||||
app = ((AppCompatActivity) m).getApplication();
|
app = ((AppCompatActivity) m).getApplication();
|
||||||
} else
|
} else
|
||||||
throw new ClassCastException("MemorizingTrustManager context must be either Activity or Service!");
|
throw new ClassCastException(
|
||||||
|
"MemorizingTrustManager context must be either Activity or Service!");
|
||||||
|
|
||||||
File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
|
File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
|
||||||
keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
|
keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
|
||||||
|
@ -260,12 +277,9 @@ public class MemorizingTrustManager {
|
||||||
/**
|
/**
|
||||||
* Removes the given certificate from MTMs key store.
|
* Removes the given certificate from MTMs key store.
|
||||||
*
|
*
|
||||||
* <p>
|
* <p><b>WARNING</b>: this does not immediately invalidate the certificate. It is well possible
|
||||||
* <b>WARNING</b>: this does not immediately invalidate the certificate. It is
|
* that (a) data is transmitted over still existing connections or (b) new connections are
|
||||||
* well possible that (a) data is transmitted over still existing connections or
|
* created using TLS renegotiation, without a new cert check.
|
||||||
* (b) new connections are created using TLS renegotiation, without a new cert
|
|
||||||
* check.
|
|
||||||
* </p>
|
|
||||||
*
|
*
|
||||||
* @param alias the certificate's alias as returned by {@link #getCertificates()}.
|
* @param alias the certificate's alias as returned by {@link #getCertificates()}.
|
||||||
* @throws KeyStoreException if the certificate could not be deleted.
|
* @throws KeyStoreException if the certificate could not be deleted.
|
||||||
|
@ -361,45 +375,60 @@ public class MemorizingTrustManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void checkCertTrusted(
|
||||||
private void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
|
X509Certificate[] chain,
|
||||||
|
String authType,
|
||||||
|
String domain,
|
||||||
|
boolean isServer,
|
||||||
|
boolean interactive)
|
||||||
throws CertificateException {
|
throws CertificateException {
|
||||||
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
|
LOGGER.log(
|
||||||
|
Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
|
||||||
try {
|
try {
|
||||||
LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager");
|
LOGGER.log(Level.FINE, "checkCertTrusted: trying appTrustManager");
|
||||||
if (isServer)
|
if (isServer) appTrustManager.checkServerTrusted(chain, authType);
|
||||||
appTrustManager.checkServerTrusted(chain, authType);
|
else appTrustManager.checkClientTrusted(chain, authType);
|
||||||
else
|
|
||||||
appTrustManager.checkClientTrusted(chain, authType);
|
|
||||||
} catch (final CertificateException ae) {
|
} catch (final CertificateException ae) {
|
||||||
LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
|
LOGGER.log(Level.FINER, "checkCertTrusted: appTrustManager failed", ae);
|
||||||
if (isCertKnown(chain[0])) {
|
if (isCertKnown(chain[0])) {
|
||||||
LOGGER.log(Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
|
LOGGER.log(
|
||||||
|
Level.INFO, "checkCertTrusted: accepting cert already stored in keystore");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (defaultTrustManager == null)
|
if (defaultTrustManager == null) throw ae;
|
||||||
throw ae;
|
|
||||||
LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager");
|
LOGGER.log(Level.FINE, "checkCertTrusted: trying defaultTrustManager");
|
||||||
if (isServer)
|
if (isServer) defaultTrustManager.checkServerTrusted(chain, authType);
|
||||||
defaultTrustManager.checkServerTrusted(chain, authType);
|
else defaultTrustManager.checkClientTrusted(chain, authType);
|
||||||
else
|
|
||||||
defaultTrustManager.checkClientTrusted(chain, authType);
|
|
||||||
} catch (final CertificateException e) {
|
} catch (final CertificateException e) {
|
||||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
|
final SharedPreferences preferences =
|
||||||
final boolean trustSystemCAs = !preferences.getBoolean("dont_trust_system_cas", false);
|
PreferenceManager.getDefaultSharedPreferences(master);
|
||||||
if (domain != null && isServer && trustSystemCAs && !isIp(domain) && !domain.endsWith(".onion")) {
|
final boolean trustSystemCAs =
|
||||||
|
!preferences.getBoolean("dont_trust_system_cas", false);
|
||||||
|
if (domain != null
|
||||||
|
&& isServer
|
||||||
|
&& trustSystemCAs
|
||||||
|
&& !isIp(domain)
|
||||||
|
&& !domain.endsWith(".onion")) {
|
||||||
final String hash = getBase64Hash(chain[0], "SHA-256");
|
final String hash = getBase64Hash(chain[0], "SHA-256");
|
||||||
final List<String> fingerprints = getPoshFingerprints(domain);
|
final List<String> fingerprints = getPoshFingerprints(domain);
|
||||||
if (hash != null && fingerprints.size() > 0) {
|
if (hash != null && fingerprints.size() > 0) {
|
||||||
if (fingerprints.contains(hash)) {
|
if (fingerprints.contains(hash)) {
|
||||||
Log.d(Config.LOGTAG, "trusted cert fingerprint of " + domain + " via posh");
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
"trusted cert fingerprint of " + domain + " via posh");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
Log.d(Config.LOGTAG, "fingerprint " + hash + " not found in " + fingerprints);
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
"fingerprint " + hash + " not found in " + fingerprints);
|
||||||
}
|
}
|
||||||
if (getPoshCacheFile(domain).delete()) {
|
if (getPoshCacheFile(domain).delete()) {
|
||||||
Log.d(Config.LOGTAG, "deleted posh file for " + domain + " after not being able to verify");
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
"deleted posh file for "
|
||||||
|
+ domain
|
||||||
|
+ " after not being able to verify");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -422,17 +451,25 @@ public class MemorizingTrustManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getPoshFingerprintsFromServer(String domain) {
|
private List<String> getPoshFingerprintsFromServer(String domain) {
|
||||||
return getPoshFingerprintsFromServer(domain, "https://" + domain + "/.well-known/posh/xmpp-client.json", -1, true);
|
return getPoshFingerprintsFromServer(
|
||||||
|
domain, "https://" + domain + "/.well-known/posh/xmpp-client.json", -1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) {
|
private List<String> getPoshFingerprintsFromServer(
|
||||||
|
String domain, String url, int maxTtl, boolean followUrl) {
|
||||||
Log.d(Config.LOGTAG, "downloading json for " + domain + " from " + url);
|
Log.d(Config.LOGTAG, "downloading json for " + domain + " from " + url);
|
||||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
|
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(master);
|
||||||
final boolean useTor = QuickConversationsService.isConversations() && preferences.getBoolean("use_tor", master.getResources().getBoolean(R.bool.use_tor));
|
final boolean useTor =
|
||||||
|
QuickConversationsService.isConversations()
|
||||||
|
&& preferences.getBoolean(
|
||||||
|
"use_tor", master.getResources().getBoolean(R.bool.use_tor));
|
||||||
try {
|
try {
|
||||||
final List<String> results = new ArrayList<>();
|
final List<String> results = new ArrayList<>();
|
||||||
final InputStream inputStream = HttpConnectionManager.open(url, useTor);
|
final InputStream inputStream = HttpConnectionManager.open(url, useTor);
|
||||||
final String body = CharStreams.toString(new InputStreamReader(ByteStreams.limit(inputStream,10_000), Charsets.UTF_8));
|
final String body =
|
||||||
|
CharStreams.toString(
|
||||||
|
new InputStreamReader(
|
||||||
|
ByteStreams.limit(inputStream, 10_000), Charsets.UTF_8));
|
||||||
final JSONObject jsonObject = new JSONObject(body);
|
final JSONObject jsonObject = new JSONObject(body);
|
||||||
int expires = jsonObject.getInt("expires");
|
int expires = jsonObject.getInt("expires");
|
||||||
if (expires <= 0) {
|
if (expires <= 0) {
|
||||||
|
@ -459,7 +496,7 @@ public class MemorizingTrustManager {
|
||||||
writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis());
|
writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis());
|
||||||
return results;
|
return results;
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
Log.d(Config.LOGTAG, "error fetching posh",e);
|
Log.d(Config.LOGTAG, "error fetching posh", e);
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -489,7 +526,8 @@ public class MemorizingTrustManager {
|
||||||
final File file = getPoshCacheFile(domain);
|
final File file = getPoshCacheFile(domain);
|
||||||
try {
|
try {
|
||||||
final InputStream inputStream = new FileInputStream(file);
|
final InputStream inputStream = new FileInputStream(file);
|
||||||
final String json = CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
|
final String json =
|
||||||
|
CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8));
|
||||||
final JSONObject jsonObject = new JSONObject(json);
|
final JSONObject jsonObject = new JSONObject(json);
|
||||||
long expires = jsonObject.getLong("expires");
|
long expires = jsonObject.getLong("expires");
|
||||||
long expiresIn = expires - System.currentTimeMillis();
|
long expiresIn = expires - System.currentTimeMillis();
|
||||||
|
@ -514,7 +552,9 @@ public class MemorizingTrustManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private X509Certificate[] getAcceptedIssuers() {
|
private X509Certificate[] getAcceptedIssuers() {
|
||||||
return defaultTrustManager == null ? new X509Certificate[0] : defaultTrustManager.getAcceptedIssuers();
|
return defaultTrustManager == null
|
||||||
|
? new X509Certificate[0]
|
||||||
|
: defaultTrustManager.getAcceptedIssuers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int createDecisionId(MTMDecision d) {
|
private int createDecisionId(MTMDecision d) {
|
||||||
|
@ -527,7 +567,8 @@ public class MemorizingTrustManager {
|
||||||
return myId;
|
return myId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void certDetails(final StringBuffer si, final X509Certificate c, final boolean showValidFor) {
|
private void certDetails(
|
||||||
|
final StringBuffer si, final X509Certificate c, final boolean showValidFor) {
|
||||||
|
|
||||||
si.append("\n");
|
si.append("\n");
|
||||||
if (showValidFor) {
|
if (showValidFor) {
|
||||||
|
@ -564,8 +605,7 @@ public class MemorizingTrustManager {
|
||||||
// not found", so we use string comparison.
|
// not found", so we use string comparison.
|
||||||
if (NO_TRUST_ANCHOR.equals(e.getMessage())) {
|
if (NO_TRUST_ANCHOR.equals(e.getMessage())) {
|
||||||
si.append(master.getString(R.string.mtm_trust_anchor));
|
si.append(master.getString(R.string.mtm_trust_anchor));
|
||||||
} else
|
} else si.append(e.getLocalizedMessage());
|
||||||
si.append(e.getLocalizedMessage());
|
|
||||||
si.append("\n");
|
si.append("\n");
|
||||||
}
|
}
|
||||||
si.append("\n");
|
si.append("\n");
|
||||||
|
@ -573,7 +613,7 @@ public class MemorizingTrustManager {
|
||||||
si.append("\n\n");
|
si.append("\n\n");
|
||||||
si.append(master.getString(R.string.mtm_cert_details));
|
si.append(master.getString(R.string.mtm_cert_details));
|
||||||
si.append('\n');
|
si.append('\n');
|
||||||
for(int i = 0; i < chain.length; ++i) {
|
for (int i = 0; i < chain.length; ++i) {
|
||||||
certDetails(si, chain[i], i == 0);
|
certDetails(si, chain[i], i == 0);
|
||||||
}
|
}
|
||||||
return si.toString();
|
return si.toString();
|
||||||
|
@ -593,7 +633,8 @@ public class MemorizingTrustManager {
|
||||||
MTMDecision choice = new MTMDecision();
|
MTMDecision choice = new MTMDecision();
|
||||||
final int myId = createDecisionId(choice);
|
final int myId = createDecisionId(choice);
|
||||||
|
|
||||||
masterHandler.post(new Runnable() {
|
masterHandler.post(
|
||||||
|
new Runnable() {
|
||||||
public void run() {
|
public void run() {
|
||||||
Intent ni = new Intent(master, MemorizingActivity.class);
|
Intent ni = new Intent(master, MemorizingActivity.class);
|
||||||
ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
ni.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
@ -661,7 +702,8 @@ public class MemorizingTrustManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
||||||
|
throws CertificateException {
|
||||||
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false);
|
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -675,7 +717,6 @@ public class MemorizingTrustManager {
|
||||||
public X509Certificate[] getAcceptedIssuers() {
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
return MemorizingTrustManager.this.getAcceptedIssuers();
|
return MemorizingTrustManager.this.getAcceptedIssuers();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InteractiveMemorizingTrustManager implements X509TrustManager {
|
private class InteractiveMemorizingTrustManager implements X509TrustManager {
|
||||||
|
@ -686,7 +727,8 @@ public class MemorizingTrustManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
||||||
|
throws CertificateException {
|
||||||
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true);
|
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,11 @@ package eu.siacs.conversations.utils;
|
||||||
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
import org.conscrypt.Conscrypt;
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
@ -17,29 +15,26 @@ import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
||||||
import javax.net.ssl.SNIHostName;
|
import javax.net.ssl.SNIHostName;
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.SSLParameters;
|
import javax.net.ssl.SSLParameters;
|
||||||
import javax.net.ssl.SSLSession;
|
import javax.net.ssl.SSLSession;
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import org.conscrypt.Conscrypt;
|
||||||
import eu.siacs.conversations.Config;
|
|
||||||
import eu.siacs.conversations.entities.Account;
|
|
||||||
|
|
||||||
public class SSLSockets {
|
public class SSLSockets {
|
||||||
|
|
||||||
public static void setSecurity(final SSLSocket sslSocket) {
|
public static void setSecurity(final SSLSocket sslSocket) {
|
||||||
final String[] supportProtocols;
|
final String[] supportProtocols;
|
||||||
final Collection<String> supportedProtocols = new LinkedList<>(
|
final Collection<String> supportedProtocols =
|
||||||
Arrays.asList(sslSocket.getSupportedProtocols()));
|
new LinkedList<>(Arrays.asList(sslSocket.getSupportedProtocols()));
|
||||||
supportedProtocols.remove("SSLv3");
|
supportedProtocols.remove("SSLv3");
|
||||||
supportProtocols = supportedProtocols.toArray(new String[0]);
|
supportProtocols = supportedProtocols.toArray(new String[0]);
|
||||||
|
|
||||||
sslSocket.setEnabledProtocols(supportProtocols);
|
sslSocket.setEnabledProtocols(supportProtocols);
|
||||||
|
|
||||||
final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites(
|
final String[] cipherSuites =
|
||||||
sslSocket.getSupportedCipherSuites());
|
CryptoHelper.getOrderedCipherSuites(sslSocket.getSupportedCipherSuites());
|
||||||
if (cipherSuites.length > 0) {
|
if (cipherSuites.length > 0) {
|
||||||
sslSocket.setEnabledCipherSuites(cipherSuites);
|
sslSocket.setEnabledCipherSuites(cipherSuites);
|
||||||
}
|
}
|
||||||
|
@ -70,7 +65,8 @@ public class SSLSockets {
|
||||||
socket.setSSLParameters(parameters);
|
socket.setSSLParameters(parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setApplicationProtocolReflection(final SSLSocket socket, final String protocol) {
|
private static void setApplicationProtocolReflection(
|
||||||
|
final SSLSocket socket, final String protocol) {
|
||||||
try {
|
try {
|
||||||
final Method method = socket.getClass().getMethod("setAlpnProtocols", byte[].class);
|
final Method method = socket.getClass().getMethod("setAlpnProtocols", byte[].class);
|
||||||
// the concatenation of 8-bit, length prefixed protocol names, just one in our case...
|
// the concatenation of 8-bit, length prefixed protocol names, just one in our case...
|
||||||
|
@ -78,16 +74,17 @@ public class SSLSockets {
|
||||||
final byte[] protocolUTF8Bytes = protocol.getBytes(StandardCharsets.UTF_8);
|
final byte[] protocolUTF8Bytes = protocol.getBytes(StandardCharsets.UTF_8);
|
||||||
final byte[] lengthPrefixedProtocols = new byte[protocolUTF8Bytes.length + 1];
|
final byte[] lengthPrefixedProtocols = new byte[protocolUTF8Bytes.length + 1];
|
||||||
lengthPrefixedProtocols[0] = (byte) protocol.length(); // cannot be over 255 anyhow
|
lengthPrefixedProtocols[0] = (byte) protocol.length(); // cannot be over 255 anyhow
|
||||||
System.arraycopy(protocolUTF8Bytes, 0, lengthPrefixedProtocols, 1, protocolUTF8Bytes.length);
|
System.arraycopy(
|
||||||
method.invoke(socket, new Object[]{lengthPrefixedProtocols});
|
protocolUTF8Bytes, 0, lengthPrefixedProtocols, 1, protocolUTF8Bytes.length);
|
||||||
|
method.invoke(socket, new Object[] {lengthPrefixedProtocols});
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
Log.e(Config.LOGTAG,"unable to set ALPN on socket",e);
|
Log.e(Config.LOGTAG, "unable to set ALPN on socket", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setApplicationProtocol(final SSLSocket socket, final String protocol) {
|
public static void setApplicationProtocol(final SSLSocket socket, final String protocol) {
|
||||||
if (Conscrypt.isConscrypt(socket)) {
|
if (Conscrypt.isConscrypt(socket)) {
|
||||||
Conscrypt.setApplicationProtocols(socket, new String[]{protocol});
|
Conscrypt.setApplicationProtocols(socket, new String[] {protocol});
|
||||||
} else {
|
} else {
|
||||||
setApplicationProtocolReflection(socket, protocol);
|
setApplicationProtocolReflection(socket, protocol);
|
||||||
}
|
}
|
||||||
|
@ -101,11 +98,15 @@ public class SSLSockets {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void log(Account account, SSLSocket socket) {
|
public static void log(final Account account, SSLSocket socket) {
|
||||||
|
log(account.getJid(), socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void log(final Jid address, SSLSocket socket) {
|
||||||
SSLSession session = socket.getSession();
|
SSLSession session = socket.getSession();
|
||||||
Log.d(
|
Log.d(
|
||||||
Config.LOGTAG,
|
Config.LOGTAG,
|
||||||
account.getJid().asBareJid()
|
address
|
||||||
+ ": protocol="
|
+ ": protocol="
|
||||||
+ session.getProtocol()
|
+ session.getProtocol()
|
||||||
+ " cipher="
|
+ " cipher="
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package eu.siacs.conversations.xml;
|
package eu.siacs.conversations.xml;
|
||||||
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import eu.siacs.conversations.utils.XmlHelper;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
import java.util.Hashtable;
|
import java.util.Hashtable;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import eu.siacs.conversations.utils.XmlHelper;
|
|
||||||
|
|
||||||
public class Tag {
|
public class Tag {
|
||||||
public static final int NO = -1;
|
public static final int NO = -1;
|
||||||
|
@ -52,6 +51,13 @@ public class Tag {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Tag setAttribute(final String attrName, final Jid attrValue) {
|
||||||
|
if (attrValue != null) {
|
||||||
|
this.attributes.put(attrName, attrValue.toEscapedString());
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public void setAttributes(final Hashtable<String, String> attributes) {
|
public void setAttributes(final Hashtable<String, String> attributes) {
|
||||||
this.attributes = attributes;
|
this.attributes = attributes;
|
||||||
}
|
}
|
||||||
|
|
13
src/main/java/im/conversations/android/Conversations.java
Normal file
13
src/main/java/im/conversations/android/Conversations.java
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package im.conversations.android;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import im.conversations.android.xmpp.ConnectionPool;
|
||||||
|
|
||||||
|
public class Conversations extends Application {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
ConnectionPool.getInstance(this).reconfigure();
|
||||||
|
}
|
||||||
|
}
|
38
src/main/java/im/conversations/android/Uuids.java
Normal file
38
src/main/java/im/conversations/android/Uuids.java
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package im.conversations.android;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Uuids {
|
||||||
|
|
||||||
|
private static final long VERSION_MASK = 4 << 12;
|
||||||
|
|
||||||
|
public static UUID getUuid(final byte[] bytes) {
|
||||||
|
Preconditions.checkArgument(bytes != null && bytes.length == 32);
|
||||||
|
|
||||||
|
long msb = 0;
|
||||||
|
long lsb = 0;
|
||||||
|
|
||||||
|
msb |= (bytes[0x0] & 0xffL) << 56;
|
||||||
|
msb |= (bytes[0x1] & 0xffL) << 48;
|
||||||
|
msb |= (bytes[0x2] & 0xffL) << 40;
|
||||||
|
msb |= (bytes[0x3] & 0xffL) << 32;
|
||||||
|
msb |= (bytes[0x4] & 0xffL) << 24;
|
||||||
|
msb |= (bytes[0x5] & 0xffL) << 16;
|
||||||
|
msb |= (bytes[0x6] & 0xffL) << 8;
|
||||||
|
msb |= (bytes[0x7] & 0xffL);
|
||||||
|
|
||||||
|
lsb |= (bytes[0x8] & 0xffL) << 56;
|
||||||
|
lsb |= (bytes[0x9] & 0xffL) << 48;
|
||||||
|
lsb |= (bytes[0xa] & 0xffL) << 40;
|
||||||
|
lsb |= (bytes[0xb] & 0xffL) << 32;
|
||||||
|
lsb |= (bytes[0xc] & 0xffL) << 24;
|
||||||
|
lsb |= (bytes[0xd] & 0xffL) << 16;
|
||||||
|
lsb |= (bytes[0xe] & 0xffL) << 8;
|
||||||
|
lsb |= (bytes[0xf] & 0xffL);
|
||||||
|
|
||||||
|
msb = (msb & 0xffffffffffff0fffL) | VERSION_MASK; // set version
|
||||||
|
lsb = (lsb & 0x3fffffffffffffffL) | 0x8000000000000000L; // set variant
|
||||||
|
return new UUID(msb, lsb);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ import androidx.room.Database;
|
||||||
import androidx.room.Room;
|
import androidx.room.Room;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
import androidx.room.TypeConverters;
|
import androidx.room.TypeConverters;
|
||||||
|
import im.conversations.android.database.dao.AccountDao;
|
||||||
|
import im.conversations.android.database.dao.PresenceDao;
|
||||||
import im.conversations.android.database.entity.AccountEntity;
|
import im.conversations.android.database.entity.AccountEntity;
|
||||||
import im.conversations.android.database.entity.BlockedItemEntity;
|
import im.conversations.android.database.entity.BlockedItemEntity;
|
||||||
import im.conversations.android.database.entity.ChatEntity;
|
import im.conversations.android.database.entity.ChatEntity;
|
||||||
|
@ -62,4 +64,8 @@ public abstract class ConversationsDatabase extends RoomDatabase {
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract AccountDao accountDao();
|
||||||
|
|
||||||
|
public abstract PresenceDao presenceDao();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,206 @@
|
||||||
|
package im.conversations.android.database;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.security.keystore.KeyGenParameterSpec;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.security.crypto.EncryptedFile;
|
||||||
|
import androidx.security.crypto.MasterKeys;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.reflect.TypeToken;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
import im.conversations.android.xmpp.sasl.ChannelBindingMechanism;
|
||||||
|
import im.conversations.android.xmpp.sasl.HashedToken;
|
||||||
|
import im.conversations.android.xmpp.sasl.SaslMechanism;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
// TODO cache credentials?!
|
||||||
|
public class CredentialStore {
|
||||||
|
|
||||||
|
private static final String FILENAME = "credential.store";
|
||||||
|
|
||||||
|
private static final Gson GSON = new GsonBuilder().create();
|
||||||
|
|
||||||
|
private static volatile CredentialStore INSTANCE;
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
private CredentialStore(final Context context) {
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CredentialStore getInstance(final Context context) {
|
||||||
|
if (INSTANCE != null) {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
synchronized (CredentialStore.class) {
|
||||||
|
if (INSTANCE != null) {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
INSTANCE = new CredentialStore(context);
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Credential get(final Account account) {
|
||||||
|
return getOrEmpty(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(final Account account, final String password)
|
||||||
|
throws GeneralSecurityException, IOException {
|
||||||
|
setPassword(account, password, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setPassword(
|
||||||
|
final Account account, final String password, final boolean autogeneratedPassword)
|
||||||
|
throws GeneralSecurityException, IOException {
|
||||||
|
final Credential credential = getOrEmpty(account);
|
||||||
|
final Credential modifiedCredential =
|
||||||
|
new Credential(
|
||||||
|
password,
|
||||||
|
autogeneratedPassword,
|
||||||
|
credential.pinnedMechanism,
|
||||||
|
credential.pinnedChannelBinding,
|
||||||
|
credential.fastMechanism,
|
||||||
|
credential.fastToken,
|
||||||
|
credential.preAuthRegistrationToken,
|
||||||
|
credential.privateKeyAlias);
|
||||||
|
// TODO ignore if unchanged
|
||||||
|
this.set(account, modifiedCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFastToken(
|
||||||
|
final Account account, final HashedToken.Mechanism mechanism, final String token)
|
||||||
|
throws GeneralSecurityException, IOException {
|
||||||
|
final Credential credential = getOrEmpty(account);
|
||||||
|
final Credential modifiedCredential =
|
||||||
|
new Credential(
|
||||||
|
credential.password,
|
||||||
|
credential.autogeneratedPassword,
|
||||||
|
credential.pinnedMechanism,
|
||||||
|
credential.pinnedChannelBinding,
|
||||||
|
mechanism.name(),
|
||||||
|
token,
|
||||||
|
credential.preAuthRegistrationToken,
|
||||||
|
credential.privateKeyAlias);
|
||||||
|
// TODO ignore if unchanged
|
||||||
|
this.set(account, modifiedCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetFastToken(final Account account) throws GeneralSecurityException, IOException {
|
||||||
|
final Credential credential = getOrEmpty(account);
|
||||||
|
final Credential modifiedCredential =
|
||||||
|
new Credential(
|
||||||
|
credential.password,
|
||||||
|
credential.autogeneratedPassword,
|
||||||
|
credential.pinnedMechanism,
|
||||||
|
credential.pinnedChannelBinding,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
credential.preAuthRegistrationToken,
|
||||||
|
credential.privateKeyAlias);
|
||||||
|
// TODO ignore if unchanged
|
||||||
|
this.set(account, modifiedCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPinnedMechanism(final Account account, final SaslMechanism mechanism)
|
||||||
|
throws GeneralSecurityException, IOException {
|
||||||
|
final String pinnedMechanism = mechanism.getMechanism();
|
||||||
|
final String pinnedChannelBinding;
|
||||||
|
if (mechanism instanceof ChannelBindingMechanism) {
|
||||||
|
pinnedChannelBinding =
|
||||||
|
((ChannelBindingMechanism) mechanism).getChannelBinding().toString();
|
||||||
|
} else {
|
||||||
|
pinnedChannelBinding = null;
|
||||||
|
}
|
||||||
|
final Credential credential = getOrEmpty(account);
|
||||||
|
final Credential modifiedCredential =
|
||||||
|
new Credential(
|
||||||
|
credential.password,
|
||||||
|
credential.autogeneratedPassword,
|
||||||
|
pinnedMechanism,
|
||||||
|
pinnedChannelBinding,
|
||||||
|
credential.fastMechanism,
|
||||||
|
credential.fastToken,
|
||||||
|
credential.preAuthRegistrationToken,
|
||||||
|
credential.privateKeyAlias);
|
||||||
|
// TODO ignore if unchanged
|
||||||
|
this.set(account, modifiedCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetPinnedMechanism(final Account account)
|
||||||
|
throws GeneralSecurityException, IOException {
|
||||||
|
final Credential credential = getOrEmpty(account);
|
||||||
|
final Credential modifiedCredential =
|
||||||
|
new Credential(
|
||||||
|
credential.password,
|
||||||
|
credential.autogeneratedPassword,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
credential.fastMechanism,
|
||||||
|
credential.fastToken,
|
||||||
|
credential.preAuthRegistrationToken,
|
||||||
|
credential.privateKeyAlias);
|
||||||
|
// TODO ignore if unchanged
|
||||||
|
this.set(account, modifiedCredential);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Credential getOrEmpty(final Account account) {
|
||||||
|
final Map<String, Credential> store = loadOrEmpty();
|
||||||
|
final Credential credential = store.get(account.address.toEscapedString());
|
||||||
|
return credential == null ? Credential.empty() : credential;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void set(@NonNull final Account account, @NonNull final Credential credential)
|
||||||
|
throws GeneralSecurityException, IOException {
|
||||||
|
final HashMap<String, Credential> credentialStore = new HashMap<>(loadOrEmpty());
|
||||||
|
credentialStore.put(account.address.toEscapedString(), credential);
|
||||||
|
store(credentialStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Credential> loadOrEmpty() {
|
||||||
|
final Map<String, Credential> store;
|
||||||
|
try {
|
||||||
|
store = load();
|
||||||
|
} catch (final GeneralSecurityException | IOException e) {
|
||||||
|
return ImmutableMap.of();
|
||||||
|
}
|
||||||
|
return store == null ? ImmutableMap.of() : store;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Credential> load() throws GeneralSecurityException, IOException {
|
||||||
|
final EncryptedFile encryptedFile = getEncryptedFile();
|
||||||
|
final FileInputStream inputStream = encryptedFile.openFileInput();
|
||||||
|
final Type type = new TypeToken<Map<String, Credential>>() {}.getType();
|
||||||
|
return GSON.fromJson(new InputStreamReader(inputStream), type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void store(final Map<String, Credential> store)
|
||||||
|
throws GeneralSecurityException, IOException {
|
||||||
|
final EncryptedFile encryptedFile = getEncryptedFile();
|
||||||
|
final FileOutputStream outputStream = encryptedFile.openFileOutput();
|
||||||
|
GSON.toJson(store, new OutputStreamWriter(outputStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
private EncryptedFile getEncryptedFile() throws GeneralSecurityException, IOException {
|
||||||
|
final KeyGenParameterSpec keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC;
|
||||||
|
final String mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec);
|
||||||
|
return new EncryptedFile.Builder(
|
||||||
|
new File(context.getFilesDir(), FILENAME),
|
||||||
|
context,
|
||||||
|
mainKeyAlias,
|
||||||
|
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package im.conversations.android.database.dao;
|
||||||
|
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.OnConflictStrategy;
|
||||||
|
import androidx.room.Query;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import im.conversations.android.database.entity.AccountEntity;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Connection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public interface AccountDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
|
void insert(final AccountEntity account);
|
||||||
|
|
||||||
|
@Query("SELECT id,address,randomSeed FROM account WHERE enabled = 1")
|
||||||
|
ListenableFuture<List<Account>> getEnabledAccounts();
|
||||||
|
|
||||||
|
@Query("SELECT hostname,port,directTls FROM account WHERE id=:id AND hostname != null")
|
||||||
|
Connection getConnectionSettings(long id);
|
||||||
|
|
||||||
|
@Query("SELECT resource FROM account WHERE id=:id")
|
||||||
|
String getResource(long id);
|
||||||
|
|
||||||
|
@Query("SELECT rosterVersion FROM account WHERE id=:id")
|
||||||
|
String getRosterVersion(long id);
|
||||||
|
|
||||||
|
@Query("SELECT quickStartAvailable FROM account where id=:id")
|
||||||
|
boolean quickStartAvailable(long id);
|
||||||
|
|
||||||
|
@Query("SELECT pendingRegistration FROM account where id=:id")
|
||||||
|
boolean pendingRegistration(long id);
|
||||||
|
|
||||||
|
@Query("SELECT loggedInSuccessfully == 0 FROM account where id=:id")
|
||||||
|
boolean isInitialLogin(long id);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"UPDATE account set quickStartAvailable=:available WHERE id=:id AND"
|
||||||
|
+ " quickStartAvailable != :available")
|
||||||
|
void setQuickStartAvailable(long id, boolean available);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"UPDATE account set pendingRegistration=:pendingRegistration WHERE id=:id AND"
|
||||||
|
+ " pendingRegistration != :pendingRegistration")
|
||||||
|
void setPendingRegistration(long id, boolean pendingRegistration);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"UPDATE account set loggedInSuccessfully=:loggedInSuccessfully WHERE id=:id AND"
|
||||||
|
+ " loggedInSuccessfully != :loggedInSuccessfully")
|
||||||
|
int setLoggedInSuccessfully(long id, boolean loggedInSuccessfully);
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"UPDATE account set showErrorNotification=:showErrorNotification WHERE id=:id AND"
|
||||||
|
+ " showErrorNotification != :showErrorNotification")
|
||||||
|
int setShowErrorNotification(long id, boolean showErrorNotification);
|
||||||
|
|
||||||
|
@Query("UPDATE account set resource=:resource WHERE id=:id")
|
||||||
|
void setResource(long id, String resource);
|
||||||
|
|
||||||
|
// TODO on disable set resource to null
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package im.conversations.android.database.dao;
|
||||||
|
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Query;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public interface PresenceDao {
|
||||||
|
|
||||||
|
@Query("DELETE FROM presence WHERE accountId=:account")
|
||||||
|
void deletePresences(long account);
|
||||||
|
}
|
|
@ -26,6 +26,15 @@ public class AccountEntity {
|
||||||
|
|
||||||
public boolean enabled;
|
public boolean enabled;
|
||||||
|
|
||||||
|
public boolean quickStartAvailable = false;
|
||||||
|
public boolean pendingRegistration = false;
|
||||||
|
|
||||||
|
// TODO this is only used during setup; depending on how the setup procedure will look in the
|
||||||
|
// future we might get rid of this property
|
||||||
|
public boolean loggedInSuccessfully = false;
|
||||||
|
|
||||||
|
public boolean showErrorNotification = true;
|
||||||
|
|
||||||
public String rosterVersion;
|
public String rosterVersion;
|
||||||
|
|
||||||
@Embedded public Connection connection;
|
@Embedded public Connection connection;
|
||||||
|
|
|
@ -50,4 +50,7 @@ public class PresenceEntity {
|
||||||
@Nullable public MucOptions.Role mucUserRole;
|
@Nullable public MucOptions.Role mucUserRole;
|
||||||
|
|
||||||
@Nullable public Jid mucUserJid;
|
@Nullable public Jid mucUserJid;
|
||||||
|
|
||||||
|
// set to true if presence has status code 110 (this means we are online)
|
||||||
|
public boolean mucUserSelf;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package im.conversations.android.database.model;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import com.google.common.io.ByteSource;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.Uuids;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class Account {
|
||||||
|
|
||||||
|
public final long id;
|
||||||
|
@NonNull public final Jid address;
|
||||||
|
@NonNull public final byte[] randomSeed;
|
||||||
|
|
||||||
|
public Account(final long id, @NonNull final Jid address, @NonNull byte[] randomSeed) {
|
||||||
|
Preconditions.checkNotNull(address, "Account can not be instantiated without an address");
|
||||||
|
Preconditions.checkArgument(address.isBareJid(), "Account address must be bare");
|
||||||
|
Preconditions.checkArgument(
|
||||||
|
randomSeed.length == 32, "RandomSeed must have exactly 32 bytes");
|
||||||
|
this.id = id;
|
||||||
|
this.address = address;
|
||||||
|
this.randomSeed = randomSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Account account = (Account) o;
|
||||||
|
return id == account.id
|
||||||
|
&& Objects.equal(address, account.address)
|
||||||
|
&& Objects.equal(randomSeed, account.randomSeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(id, address, randomSeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOnion() {
|
||||||
|
final String domain = address.getDomain().toEscapedString();
|
||||||
|
return domain.endsWith(".onion");
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getPublicDeviceId() {
|
||||||
|
try {
|
||||||
|
return Uuids.getUuid(
|
||||||
|
ByteSource.wrap(randomSeed).slice(0, 16).hash(Hashing.sha256()).asBytes());
|
||||||
|
} catch (final IOException e) {
|
||||||
|
return UUID.randomUUID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package im.conversations.android.database.model;
|
||||||
|
|
||||||
|
public class Credential {
|
||||||
|
|
||||||
|
public final String password;
|
||||||
|
public final boolean autogeneratedPassword;
|
||||||
|
public final String pinnedMechanism;
|
||||||
|
public final String pinnedChannelBinding;
|
||||||
|
|
||||||
|
public final String fastMechanism;
|
||||||
|
public final String fastToken;
|
||||||
|
|
||||||
|
public final String preAuthRegistrationToken;
|
||||||
|
|
||||||
|
public final String privateKeyAlias;
|
||||||
|
|
||||||
|
private Credential() {
|
||||||
|
this.password = null;
|
||||||
|
this.autogeneratedPassword = false;
|
||||||
|
this.pinnedMechanism = null;
|
||||||
|
this.pinnedChannelBinding = null;
|
||||||
|
this.fastMechanism = null;
|
||||||
|
this.fastToken = null;
|
||||||
|
this.preAuthRegistrationToken = null;
|
||||||
|
this.privateKeyAlias = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Credential(
|
||||||
|
String password,
|
||||||
|
boolean autogeneratedPassword,
|
||||||
|
String pinnedMechanism,
|
||||||
|
String pinnedChannelBinding,
|
||||||
|
String fastMechanism,
|
||||||
|
String fastToken,
|
||||||
|
String preAuthRegistrationToken,
|
||||||
|
String privateKeyAlias) {
|
||||||
|
this.password = password;
|
||||||
|
this.autogeneratedPassword = autogeneratedPassword;
|
||||||
|
this.pinnedMechanism = pinnedMechanism;
|
||||||
|
this.pinnedChannelBinding = pinnedChannelBinding;
|
||||||
|
this.fastMechanism = fastMechanism;
|
||||||
|
this.fastToken = fastToken;
|
||||||
|
this.preAuthRegistrationToken = preAuthRegistrationToken;
|
||||||
|
this.privateKeyAlias = privateKeyAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Credential empty() {
|
||||||
|
return new Credential();
|
||||||
|
}
|
||||||
|
}
|
356
src/main/java/im/conversations/android/xmpp/ConnectionPool.java
Normal file
356
src/main/java/im/conversations/android/xmpp/ConnectionPool.java
Normal file
|
@ -0,0 +1,356 @@
|
||||||
|
package im.conversations.android.xmpp;
|
||||||
|
|
||||||
|
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.util.Log;
|
||||||
|
import com.google.common.base.Optional;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
|
import eu.siacs.conversations.utils.PhoneHelper;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.database.ConversationsDatabase;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class ConnectionPool {
|
||||||
|
|
||||||
|
private static volatile ConnectionPool INSTANCE;
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
private final Executor reconfigurationExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
private final ScheduledExecutorService reconnectExecutor =
|
||||||
|
Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
|
private final List<XmppConnection> connections = new ArrayList<>();
|
||||||
|
private final HashSet<Jid> lowPingTimeoutMode = new HashSet<>();
|
||||||
|
|
||||||
|
private ConnectionPool(final Context context) {
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<Void> reconfigure() {
|
||||||
|
final ListenableFuture<List<Account>> accountFuture =
|
||||||
|
ConversationsDatabase.getInstance(context).accountDao().getEnabledAccounts();
|
||||||
|
return Futures.transform(
|
||||||
|
accountFuture,
|
||||||
|
accounts -> this.reconfigure(ImmutableSet.copyOf(accounts)),
|
||||||
|
reconfigurationExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized XmppConnection get(final Jid address) {
|
||||||
|
return Iterables.find(this.connections, c -> address.equals(c.getAccount().address));
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized XmppConnection get(final long id) {
|
||||||
|
return Iterables.find(this.connections, c -> id == c.getAccount().id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isEnabled(final long id) {
|
||||||
|
return Iterables.any(this.connections, c -> id == c.getAccount().id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized List<XmppConnection> getConnections() {
|
||||||
|
return ImmutableList.copyOf(this.connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized Void reconfigure(final Set<Account> accounts) {
|
||||||
|
final Set<Account> current = getAccounts();
|
||||||
|
final Set<Account> removed = Sets.difference(current, accounts);
|
||||||
|
final Set<Account> added = Sets.difference(accounts, current);
|
||||||
|
for (final Account account : added) {
|
||||||
|
final XmppConnection connection = this.instantiate(context, account);
|
||||||
|
connection.setOnStatusChangedListener(this::onStatusChanged);
|
||||||
|
}
|
||||||
|
for (final Account account : removed) {
|
||||||
|
final Optional<XmppConnection> connectionOptional =
|
||||||
|
Iterables.tryFind(connections, c -> c.getAccount().equals(account));
|
||||||
|
if (connectionOptional.isPresent()) {
|
||||||
|
final XmppConnection connection = connectionOptional.get();
|
||||||
|
disconnect(connection, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onStatusChanged(final XmppConnection connection) {
|
||||||
|
final Account account = connection.getAccount();
|
||||||
|
if (connection.getStatus() == ConnectionState.ONLINE || connection.getStatus().isError()) {
|
||||||
|
// TODO notify QuickConversationsService of account state change
|
||||||
|
// mQuickConversationsService.signalAccountStateChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection.getStatus() == ConnectionState.ONLINE) {
|
||||||
|
synchronized (lowPingTimeoutMode) {
|
||||||
|
if (lowPingTimeoutMode.remove(account.address)) {
|
||||||
|
Log.d(Config.LOGTAG, account.address + ": leaving low ping timeout mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConversationsDatabase.getInstance(context)
|
||||||
|
.accountDao()
|
||||||
|
.setShowErrorNotification(account.id, true);
|
||||||
|
if (connection.getFeatures().csi()) {
|
||||||
|
// TODO send correct CSI state (connection.sendActive or connection.sendInactive)
|
||||||
|
}
|
||||||
|
scheduleWakeUpCall(Config.PING_MAX_INTERVAL);
|
||||||
|
} else if (connection.getStatus() == ConnectionState.OFFLINE) {
|
||||||
|
|
||||||
|
// TODO previously we would call resetSendingToWaiting. The new architecture likely
|
||||||
|
// won’t need this but we should double check
|
||||||
|
|
||||||
|
// resetSendingToWaiting(account);
|
||||||
|
if (isInLowPingTimeoutMode(account)) {
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
account.address
|
||||||
|
+ ": went into offline state during low ping mode."
|
||||||
|
+ " reconnecting now");
|
||||||
|
reconnectAccount(connection);
|
||||||
|
} else {
|
||||||
|
final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2;
|
||||||
|
scheduleWakeUpCall(timeToReconnect);
|
||||||
|
}
|
||||||
|
} else if (connection.getStatus() == ConnectionState.REGISTRATION_SUCCESSFUL) {
|
||||||
|
// databaseBackend.updateAccount(account);
|
||||||
|
reconnectAccount(connection);
|
||||||
|
} else if (connection.getStatus() != ConnectionState.CONNECTING) {
|
||||||
|
// resetSendingToWaiting(account);
|
||||||
|
if (connection.getStatus().isAttemptReconnect()) {
|
||||||
|
final int next = connection.getTimeToNextAttempt();
|
||||||
|
final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
|
||||||
|
if (next <= 0) {
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
account.address
|
||||||
|
+ ": error connecting account. reconnecting now."
|
||||||
|
+ " lowPingTimeout="
|
||||||
|
+ lowPingTimeoutMode);
|
||||||
|
reconnectAccount(connection);
|
||||||
|
} else {
|
||||||
|
final int attempt = connection.getAttempt() + 1;
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
account.address
|
||||||
|
+ ": error connecting account. try again in "
|
||||||
|
+ next
|
||||||
|
+ "s for the "
|
||||||
|
+ attempt
|
||||||
|
+ " time. lowPingTimeout="
|
||||||
|
+ lowPingTimeoutMode);
|
||||||
|
scheduleWakeUpCall(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO toggle error notification
|
||||||
|
// getNotificationService().updateErrorNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scheduleWakeUpCall(final int seconds) {
|
||||||
|
reconnectExecutor.schedule(
|
||||||
|
() -> {
|
||||||
|
manageConnectionStates();
|
||||||
|
},
|
||||||
|
Math.max(0, seconds) + 1,
|
||||||
|
TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This is called externally if we want to force pings for example on connection switches */
|
||||||
|
public void ping() {
|
||||||
|
manageConnectionStates(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is called externally from the push receiver
|
||||||
|
*
|
||||||
|
* @param pushedAccountHash
|
||||||
|
*/
|
||||||
|
public void receivePush(final String pushedAccountHash) {
|
||||||
|
manageConnectionStates(pushedAccountHash, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void manageConnectionStates() {
|
||||||
|
manageConnectionStates(null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void manageConnectionStates(
|
||||||
|
final String pushedAccountHash, final boolean immediatePing) {
|
||||||
|
// WakeLockHelper.acquire(wakeLock);
|
||||||
|
int pingNow = 0;
|
||||||
|
final HashSet<XmppConnection> pingCandidates = new HashSet<>();
|
||||||
|
final String androidId = PhoneHelper.getAndroidId(context);
|
||||||
|
for (final XmppConnection xmppConnection : this.connections) {
|
||||||
|
final Account account = xmppConnection.getAccount();
|
||||||
|
final boolean pushWasMeantForThisAccount =
|
||||||
|
CryptoHelper.getFingerprint(account.address, androidId)
|
||||||
|
.equals(pushedAccountHash);
|
||||||
|
if (processAccountState(xmppConnection, pushWasMeantForThisAccount, pingCandidates)) {
|
||||||
|
pingNow++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pingNow > 0 || immediatePing) {
|
||||||
|
for (final XmppConnection xmppConnection : pingCandidates) {
|
||||||
|
final Account account = xmppConnection.getAccount();
|
||||||
|
final boolean lowTimeout = isInLowPingTimeoutMode(account);
|
||||||
|
xmppConnection.sendPing();
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
account.address + " send ping (lowTimeout=" + lowTimeout + ")");
|
||||||
|
scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// WakeLockHelper.release(wakeLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean processAccountState(
|
||||||
|
final XmppConnection connection,
|
||||||
|
final boolean isAccountPushed,
|
||||||
|
final HashSet<XmppConnection> pingCandidates) {
|
||||||
|
boolean pingNow = false;
|
||||||
|
if (connection.getStatus().isAttemptReconnect()) {
|
||||||
|
final Account account = connection.getAccount();
|
||||||
|
if (connection.getStatus() == ConnectionState.ONLINE) {
|
||||||
|
synchronized (lowPingTimeoutMode) {
|
||||||
|
final long lastReceived = connection.getLastPacketReceived();
|
||||||
|
final long lastSent = connection.getLastPingSent();
|
||||||
|
final long msToNextPing =
|
||||||
|
(Math.max(lastReceived, lastSent) + Config.PING_MAX_INTERVAL)
|
||||||
|
- SystemClock.elapsedRealtime();
|
||||||
|
final int pingTimeout =
|
||||||
|
lowPingTimeoutMode.contains(account.address)
|
||||||
|
? Config.LOW_PING_TIMEOUT * 1000
|
||||||
|
: Config.PING_TIMEOUT * 1000;
|
||||||
|
final long pingTimeoutIn =
|
||||||
|
(lastSent + pingTimeout) - SystemClock.elapsedRealtime();
|
||||||
|
if (lastSent > lastReceived) {
|
||||||
|
if (pingTimeoutIn < 0) {
|
||||||
|
Log.d(Config.LOGTAG, account.address + ": ping timeout");
|
||||||
|
this.reconnectAccount(connection);
|
||||||
|
} else {
|
||||||
|
final int secs = (int) (pingTimeoutIn / 1000);
|
||||||
|
this.scheduleWakeUpCall(secs);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pingCandidates.add(connection);
|
||||||
|
if (isAccountPushed) {
|
||||||
|
pingNow = true;
|
||||||
|
if (lowPingTimeoutMode.add(account.address)) {
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
account.address + ": entering low ping timeout mode");
|
||||||
|
}
|
||||||
|
} else if (msToNextPing <= 0) {
|
||||||
|
pingNow = true;
|
||||||
|
} else {
|
||||||
|
this.scheduleWakeUpCall(Ints.saturatedCast(msToNextPing / 1000));
|
||||||
|
if (lowPingTimeoutMode.remove(account.address)) {
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
account.address + ": leaving low ping timeout mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (connection.getStatus() == ConnectionState.OFFLINE) {
|
||||||
|
reconnectAccount(connection);
|
||||||
|
} else if (connection.getStatus() == ConnectionState.CONNECTING) {
|
||||||
|
long secondsSinceLastConnect =
|
||||||
|
(SystemClock.elapsedRealtime() - connection.getLastConnect()) / 1000;
|
||||||
|
long secondsSinceLastDisco =
|
||||||
|
(SystemClock.elapsedRealtime() - connection.getLastDiscoStarted()) / 1000;
|
||||||
|
long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
|
||||||
|
long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
|
||||||
|
if (timeout < 0) {
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
account.address
|
||||||
|
+ ": time out during connect reconnecting"
|
||||||
|
+ " (secondsSinceLast="
|
||||||
|
+ secondsSinceLastConnect
|
||||||
|
+ ")");
|
||||||
|
connection.resetAttemptCount(false);
|
||||||
|
reconnectAccount(connection);
|
||||||
|
} else if (discoTimeout < 0) {
|
||||||
|
connection.sendDiscoTimeout();
|
||||||
|
scheduleWakeUpCall(Ints.saturatedCast(discoTimeout));
|
||||||
|
} else {
|
||||||
|
scheduleWakeUpCall(Ints.saturatedCast(Math.min(timeout, discoTimeout)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (connection.getTimeToNextAttempt() <= 0) {
|
||||||
|
reconnectAccount(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pingNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reconnectAccount(final XmppConnection connection) {
|
||||||
|
final Account account = connection.getAccount();
|
||||||
|
if (isEnabled(account.id)) {
|
||||||
|
final Thread thread = new Thread(connection);
|
||||||
|
connection.prepareNewConnection();
|
||||||
|
connection.interrupt();
|
||||||
|
thread.start();
|
||||||
|
scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT);
|
||||||
|
} else {
|
||||||
|
disconnect(connection, true);
|
||||||
|
connection.resetEverything();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disconnect(final XmppConnection connection, boolean force) {
|
||||||
|
if (force) {
|
||||||
|
connection.disconnect(true);
|
||||||
|
} else {
|
||||||
|
// TODO bring back code that gracefully leaves MUCs
|
||||||
|
// TODO send offline presence
|
||||||
|
connection.disconnect(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInLowPingTimeoutMode(Account account) {
|
||||||
|
synchronized (lowPingTimeoutMode) {
|
||||||
|
return lowPingTimeoutMode.contains(account.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private XmppConnection instantiate(final Context context, final Account account) {
|
||||||
|
final XmppConnection xmppConnection = new XmppConnection(context, account);
|
||||||
|
this.connections.add(xmppConnection);
|
||||||
|
return xmppConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Account> getAccounts() {
|
||||||
|
return ImmutableSet.copyOf(Lists.transform(this.connections, XmppConnection::getAccount));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ConnectionPool getInstance(final Context context) {
|
||||||
|
if (INSTANCE != null) {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
synchronized (ConnectionPool.class) {
|
||||||
|
if (INSTANCE != null) {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
INSTANCE = new ConnectionPool(context);
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
src/main/java/im/conversations/android/xmpp/ConnectionState.java
Normal file
124
src/main/java/im/conversations/android/xmpp/ConnectionState.java
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
package im.conversations.android.xmpp;
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import eu.siacs.conversations.R;
|
||||||
|
|
||||||
|
public enum ConnectionState {
|
||||||
|
OFFLINE(false),
|
||||||
|
CONNECTING(false),
|
||||||
|
ONLINE(false),
|
||||||
|
UNAUTHORIZED,
|
||||||
|
TEMPORARY_AUTH_FAILURE,
|
||||||
|
SERVER_NOT_FOUND,
|
||||||
|
REGISTRATION_SUCCESSFUL(false),
|
||||||
|
REGISTRATION_FAILED(true, false),
|
||||||
|
REGISTRATION_WEB(true, false),
|
||||||
|
REGISTRATION_CONFLICT(true, false),
|
||||||
|
REGISTRATION_NOT_SUPPORTED(true, false),
|
||||||
|
REGISTRATION_PLEASE_WAIT(true, false),
|
||||||
|
REGISTRATION_INVALID_TOKEN(true, false),
|
||||||
|
REGISTRATION_PASSWORD_TOO_WEAK(true, false),
|
||||||
|
TLS_ERROR,
|
||||||
|
TLS_ERROR_DOMAIN,
|
||||||
|
INCOMPATIBLE_SERVER,
|
||||||
|
INCOMPATIBLE_CLIENT,
|
||||||
|
TOR_NOT_AVAILABLE,
|
||||||
|
DOWNGRADE_ATTACK,
|
||||||
|
SESSION_FAILURE,
|
||||||
|
BIND_FAILURE,
|
||||||
|
HOST_UNKNOWN,
|
||||||
|
STREAM_ERROR,
|
||||||
|
STREAM_OPENING_ERROR,
|
||||||
|
POLICY_VIOLATION,
|
||||||
|
PAYMENT_REQUIRED,
|
||||||
|
MISSING_INTERNET_PERMISSION(false);
|
||||||
|
|
||||||
|
private final boolean isError;
|
||||||
|
private final boolean attemptReconnect;
|
||||||
|
|
||||||
|
ConnectionState(final boolean isError) {
|
||||||
|
this(isError, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionState(final boolean isError, final boolean reconnect) {
|
||||||
|
this.isError = isError;
|
||||||
|
this.attemptReconnect = reconnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionState() {
|
||||||
|
this(true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isError() {
|
||||||
|
return this.isError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAttemptReconnect() {
|
||||||
|
return this.attemptReconnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO refactor into DataBinder (we can print the enum directly in the UI)
|
||||||
|
@StringRes
|
||||||
|
public int getReadableId() {
|
||||||
|
switch (this) {
|
||||||
|
case ONLINE:
|
||||||
|
return R.string.account_status_online;
|
||||||
|
case CONNECTING:
|
||||||
|
return R.string.account_status_connecting;
|
||||||
|
case OFFLINE:
|
||||||
|
return R.string.account_status_offline;
|
||||||
|
case UNAUTHORIZED:
|
||||||
|
return R.string.account_status_unauthorized;
|
||||||
|
case SERVER_NOT_FOUND:
|
||||||
|
return R.string.account_status_not_found;
|
||||||
|
case REGISTRATION_FAILED:
|
||||||
|
return R.string.account_status_regis_fail;
|
||||||
|
case REGISTRATION_WEB:
|
||||||
|
return R.string.account_status_regis_web;
|
||||||
|
case REGISTRATION_CONFLICT:
|
||||||
|
return R.string.account_status_regis_conflict;
|
||||||
|
case REGISTRATION_SUCCESSFUL:
|
||||||
|
return R.string.account_status_regis_success;
|
||||||
|
case REGISTRATION_NOT_SUPPORTED:
|
||||||
|
return R.string.account_status_regis_not_sup;
|
||||||
|
case REGISTRATION_INVALID_TOKEN:
|
||||||
|
return R.string.account_status_regis_invalid_token;
|
||||||
|
case TLS_ERROR:
|
||||||
|
return R.string.account_status_tls_error;
|
||||||
|
case TLS_ERROR_DOMAIN:
|
||||||
|
return R.string.account_status_tls_error_domain;
|
||||||
|
case INCOMPATIBLE_SERVER:
|
||||||
|
return R.string.account_status_incompatible_server;
|
||||||
|
case INCOMPATIBLE_CLIENT:
|
||||||
|
return R.string.account_status_incompatible_client;
|
||||||
|
case TOR_NOT_AVAILABLE:
|
||||||
|
return R.string.account_status_tor_unavailable;
|
||||||
|
case BIND_FAILURE:
|
||||||
|
return R.string.account_status_bind_failure;
|
||||||
|
case SESSION_FAILURE:
|
||||||
|
return R.string.session_failure;
|
||||||
|
case DOWNGRADE_ATTACK:
|
||||||
|
return R.string.sasl_downgrade;
|
||||||
|
case HOST_UNKNOWN:
|
||||||
|
return R.string.account_status_host_unknown;
|
||||||
|
case POLICY_VIOLATION:
|
||||||
|
return R.string.account_status_policy_violation;
|
||||||
|
case REGISTRATION_PLEASE_WAIT:
|
||||||
|
return R.string.registration_please_wait;
|
||||||
|
case REGISTRATION_PASSWORD_TOO_WEAK:
|
||||||
|
return R.string.registration_password_too_weak;
|
||||||
|
case STREAM_ERROR:
|
||||||
|
return R.string.account_status_stream_error;
|
||||||
|
case STREAM_OPENING_ERROR:
|
||||||
|
return R.string.account_status_stream_opening_error;
|
||||||
|
case PAYMENT_REQUIRED:
|
||||||
|
return R.string.payment_required;
|
||||||
|
case MISSING_INTERNET_PERMISSION:
|
||||||
|
return R.string.missing_internet_permission;
|
||||||
|
case TEMPORARY_AUTH_FAILURE:
|
||||||
|
return R.string.account_status_temporary_auth_failure;
|
||||||
|
default:
|
||||||
|
return R.string.account_status_unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2805
src/main/java/im/conversations/android/xmpp/XmppConnection.java
Normal file
2805
src/main/java/im/conversations/android/xmpp/XmppConnection.java
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,25 @@
|
||||||
|
package im.conversations.android.xmpp.processor;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import im.conversations.android.database.ConversationsDatabase;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
|
||||||
|
abstract class BaseProcessor {
|
||||||
|
|
||||||
|
protected final Context context;
|
||||||
|
protected final XmppConnection connection;
|
||||||
|
|
||||||
|
BaseProcessor(final Context context, final XmppConnection connection) {
|
||||||
|
this.context = context;
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Account getAccount() {
|
||||||
|
return connection.getAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ConversationsDatabase getDatabase() {
|
||||||
|
return ConversationsDatabase.getInstance(context);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package im.conversations.android.xmpp.processor;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||||
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class BindProcessor extends BaseProcessor implements Consumer<Jid> {
|
||||||
|
|
||||||
|
public BindProcessor(final Context context, final XmppConnection connection) {
|
||||||
|
super(context, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(final Jid jid) {
|
||||||
|
final var account = getAccount();
|
||||||
|
final var database = getDatabase();
|
||||||
|
|
||||||
|
final boolean firstLogin =
|
||||||
|
database.accountDao().setLoggedInSuccessfully(account.id, true) > 0;
|
||||||
|
|
||||||
|
if (firstLogin) {
|
||||||
|
// TODO publish display name if this is the first attempt
|
||||||
|
// iirc this is used when the display name is set from a certificate or something
|
||||||
|
}
|
||||||
|
|
||||||
|
database.presenceDao().deletePresences(account.id);
|
||||||
|
|
||||||
|
fetchRoster();
|
||||||
|
|
||||||
|
// TODO fetch bookmarks
|
||||||
|
|
||||||
|
// TODO send initial presence
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchRoster() {
|
||||||
|
final var account = getAccount();
|
||||||
|
final var database = getDatabase();
|
||||||
|
final String rosterVersion = database.accountDao().getRosterVersion(account.id);
|
||||||
|
final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
|
||||||
|
if (Strings.isNullOrEmpty(rosterVersion)) {
|
||||||
|
Log.d(Config.LOGTAG, account.address + ": fetching roster");
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG, account.address + ": fetching roster version " + rosterVersion);
|
||||||
|
}
|
||||||
|
iqPacket.query(Namespace.ROSTER).setAttribute("ver", rosterVersion);
|
||||||
|
connection.sendIqPacket(iqPacket, result -> {});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package im.conversations.android.xmpp.processor;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||||
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class IqProcessor implements Consumer<IqPacket> {
|
||||||
|
|
||||||
|
public IqProcessor(final Context context, final XmppConnection connection) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(final IqPacket packet) {}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package im.conversations.android.xmpp.processor;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
|
||||||
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class JingleProcessor implements Consumer<JinglePacket> {
|
||||||
|
|
||||||
|
public JingleProcessor(final Context context, final XmppConnection connection) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(JinglePacket jinglePacket) {}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package im.conversations.android.xmpp.processor;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import eu.siacs.conversations.xmpp.Jid;
|
||||||
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
|
public class MessageAcknowledgeProcessor implements BiFunction<Jid, String, Boolean> {
|
||||||
|
|
||||||
|
public MessageAcknowledgeProcessor(final Context context, final XmppConnection connection) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean apply(final Jid to, final String id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package im.conversations.android.xmpp.processor;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||||
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class MessageProcessor implements Consumer<MessagePacket> {
|
||||||
|
|
||||||
|
public MessageProcessor(final Context context, final XmppConnection connection) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(final MessagePacket messagePacket) {}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package im.conversations.android.xmpp.processor;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
|
||||||
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class PresenceProcessor implements Consumer<PresencePacket> {
|
||||||
|
|
||||||
|
public PresenceProcessor(final Context context, final XmppConnection connection) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(PresencePacket presencePacket) {}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
public class Anonymous extends SaslMechanism {
|
||||||
|
|
||||||
|
public static final String MECHANISM = "ANONYMOUS";
|
||||||
|
|
||||||
|
public Anonymous(final Account account) {
|
||||||
|
super(account, Credential.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import com.google.common.base.CaseFormat;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.BiMap;
|
||||||
|
import com.google.common.collect.Collections2;
|
||||||
|
import com.google.common.collect.ImmutableBiMap;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.utils.SSLSockets;
|
||||||
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public enum ChannelBinding {
|
||||||
|
NONE,
|
||||||
|
TLS_EXPORTER,
|
||||||
|
TLS_SERVER_END_POINT,
|
||||||
|
TLS_UNIQUE;
|
||||||
|
|
||||||
|
public static final BiMap<ChannelBinding, String> SHORT_NAMES;
|
||||||
|
|
||||||
|
static {
|
||||||
|
final ImmutableBiMap.Builder<ChannelBinding, String> builder = ImmutableBiMap.builder();
|
||||||
|
for (final ChannelBinding cb : values()) {
|
||||||
|
builder.put(cb, shortName(cb));
|
||||||
|
}
|
||||||
|
SHORT_NAMES = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Collection<ChannelBinding> of(final Element channelBinding) {
|
||||||
|
Preconditions.checkArgument(
|
||||||
|
channelBinding == null
|
||||||
|
|| ("sasl-channel-binding".equals(channelBinding.getName())
|
||||||
|
&& Namespace.CHANNEL_BINDING.equals(channelBinding.getNamespace())),
|
||||||
|
"pass null or a valid channel binding stream feature");
|
||||||
|
return Collections2.filter(
|
||||||
|
Collections2.transform(
|
||||||
|
Collections2.filter(
|
||||||
|
channelBinding == null
|
||||||
|
? Collections.emptyList()
|
||||||
|
: channelBinding.getChildren(),
|
||||||
|
c -> c != null && "channel-binding".equals(c.getName())),
|
||||||
|
c -> c == null ? null : ChannelBinding.of(c.getAttribute("type"))),
|
||||||
|
Predicates.notNull());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChannelBinding of(final String type) {
|
||||||
|
if (type == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return valueOf(
|
||||||
|
CaseFormat.LOWER_HYPHEN.converterTo(CaseFormat.UPPER_UNDERSCORE).convert(type));
|
||||||
|
} catch (final IllegalArgumentException e) {
|
||||||
|
Log.d(Config.LOGTAG, type + " is not a known channel binding");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ChannelBinding get(final String name) {
|
||||||
|
if (Strings.isNullOrEmpty(name)) {
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return valueOf(name);
|
||||||
|
} catch (final IllegalArgumentException e) {
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ChannelBinding best(
|
||||||
|
final Collection<ChannelBinding> bindings, final SSLSockets.Version sslVersion) {
|
||||||
|
if (sslVersion == SSLSockets.Version.NONE) {
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
if (bindings.contains(TLS_EXPORTER) && sslVersion == SSLSockets.Version.TLS_1_3) {
|
||||||
|
return TLS_EXPORTER;
|
||||||
|
} else if (bindings.contains(TLS_UNIQUE)
|
||||||
|
&& Arrays.asList(
|
||||||
|
SSLSockets.Version.TLS_1_0,
|
||||||
|
SSLSockets.Version.TLS_1_1,
|
||||||
|
SSLSockets.Version.TLS_1_2)
|
||||||
|
.contains(sslVersion)) {
|
||||||
|
return TLS_UNIQUE;
|
||||||
|
} else if (bindings.contains(TLS_SERVER_END_POINT)) {
|
||||||
|
return TLS_SERVER_END_POINT;
|
||||||
|
} else {
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAvailable(
|
||||||
|
final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) {
|
||||||
|
return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion)
|
||||||
|
== channelBinding;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String shortName(final ChannelBinding channelBinding) {
|
||||||
|
switch (channelBinding) {
|
||||||
|
case TLS_UNIQUE:
|
||||||
|
return "UNIQ";
|
||||||
|
case TLS_EXPORTER:
|
||||||
|
return "EXPR";
|
||||||
|
case TLS_SERVER_END_POINT:
|
||||||
|
return "ENDP";
|
||||||
|
case NONE:
|
||||||
|
return "NONE";
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Missing short name for " + channelBinding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import org.bouncycastle.jcajce.provider.digest.SHA256;
|
||||||
|
import org.conscrypt.Conscrypt;
|
||||||
|
|
||||||
|
public interface ChannelBindingMechanism {
|
||||||
|
|
||||||
|
String EXPORTER_LABEL = "EXPORTER-Channel-Binding";
|
||||||
|
|
||||||
|
ChannelBinding getChannelBinding();
|
||||||
|
|
||||||
|
static byte[] getChannelBindingData(
|
||||||
|
final SSLSocket sslSocket, final ChannelBinding channelBinding)
|
||||||
|
throws SaslMechanism.AuthenticationException {
|
||||||
|
if (sslSocket == null) {
|
||||||
|
throw new SaslMechanism.AuthenticationException(
|
||||||
|
"Channel binding attempt on non secure socket");
|
||||||
|
}
|
||||||
|
if (channelBinding == ChannelBinding.TLS_EXPORTER) {
|
||||||
|
final byte[] keyingMaterial;
|
||||||
|
try {
|
||||||
|
keyingMaterial =
|
||||||
|
Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32);
|
||||||
|
} catch (final SSLException e) {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Could not export keying material");
|
||||||
|
}
|
||||||
|
if (keyingMaterial == null) {
|
||||||
|
throw new SaslMechanism.AuthenticationException(
|
||||||
|
"Could not export keying material. Socket not ready");
|
||||||
|
}
|
||||||
|
return keyingMaterial;
|
||||||
|
} else if (channelBinding == ChannelBinding.TLS_UNIQUE) {
|
||||||
|
final byte[] unique = Conscrypt.getTlsUnique(sslSocket);
|
||||||
|
if (unique == null) {
|
||||||
|
throw new SaslMechanism.AuthenticationException(
|
||||||
|
"Could not retrieve tls unique. Socket not ready");
|
||||||
|
}
|
||||||
|
return unique;
|
||||||
|
} else if (channelBinding == ChannelBinding.TLS_SERVER_END_POINT) {
|
||||||
|
return getServerEndPointChannelBinding(sslSocket.getSession());
|
||||||
|
} else {
|
||||||
|
throw new SaslMechanism.AuthenticationException(
|
||||||
|
String.format("%s is not a valid channel binding", channelBinding));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] getServerEndPointChannelBinding(final SSLSession session)
|
||||||
|
throws SaslMechanism.AuthenticationException {
|
||||||
|
final Certificate[] certificates;
|
||||||
|
try {
|
||||||
|
certificates = session.getPeerCertificates();
|
||||||
|
} catch (final SSLPeerUnverifiedException e) {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Could not verify peer certificates");
|
||||||
|
}
|
||||||
|
if (certificates == null || certificates.length == 0) {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Could not retrieve peer certificate");
|
||||||
|
}
|
||||||
|
final X509Certificate certificate;
|
||||||
|
if (certificates[0] instanceof X509Certificate) {
|
||||||
|
certificate = (X509Certificate) certificates[0];
|
||||||
|
} else {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Certificate was not X509");
|
||||||
|
}
|
||||||
|
final String algorithm = certificate.getSigAlgName();
|
||||||
|
final int withIndex = algorithm.indexOf("with");
|
||||||
|
if (withIndex <= 0) {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Unable to parse SigAlgName");
|
||||||
|
}
|
||||||
|
final String hashAlgorithm = algorithm.substring(0, withIndex);
|
||||||
|
final MessageDigest messageDigest;
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc5929#section-4.1
|
||||||
|
if ("MD5".equalsIgnoreCase(hashAlgorithm) || "SHA1".equalsIgnoreCase(hashAlgorithm)) {
|
||||||
|
messageDigest = new SHA256.Digest();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
messageDigest = MessageDigest.getInstance(hashAlgorithm);
|
||||||
|
} catch (final NoSuchAlgorithmException e) {
|
||||||
|
throw new SaslMechanism.AuthenticationException(
|
||||||
|
"Could not instantiate message digest for " + hashAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final byte[] encodedCertificate;
|
||||||
|
try {
|
||||||
|
encodedCertificate = certificate.getEncoded();
|
||||||
|
} catch (final CertificateEncodingException e) {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Could not encode certificate");
|
||||||
|
}
|
||||||
|
messageDigest.update(encodedCertificate);
|
||||||
|
return messageDigest.digest();
|
||||||
|
}
|
||||||
|
}
|
112
src/main/java/im/conversations/android/xmpp/sasl/DigestMd5.java
Normal file
112
src/main/java/im/conversations/android/xmpp/sasl/DigestMd5.java
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
public class DigestMd5 extends SaslMechanism {
|
||||||
|
|
||||||
|
public static final String MECHANISM = "DIGEST-MD5";
|
||||||
|
private State state = State.INITIAL;
|
||||||
|
|
||||||
|
public DigestMd5(final Account account, final Credential credential) {
|
||||||
|
super(account, credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getResponse(final String challenge, final SSLSocket sslSocket)
|
||||||
|
throws AuthenticationException {
|
||||||
|
switch (state) {
|
||||||
|
case INITIAL:
|
||||||
|
state = State.RESPONSE_SENT;
|
||||||
|
final String encodedResponse;
|
||||||
|
try {
|
||||||
|
final Tokenizer tokenizer =
|
||||||
|
new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
|
||||||
|
String nonce = "";
|
||||||
|
for (final String token : tokenizer) {
|
||||||
|
final String[] parts = token.split("=", 2);
|
||||||
|
if (parts[0].equals("nonce")) {
|
||||||
|
nonce = parts[1].replace("\"", "");
|
||||||
|
} else if (parts[0].equals("rspauth")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final String digestUri = "xmpp/" + account.address.getDomain();
|
||||||
|
final String nonceCount = "00000001";
|
||||||
|
final String x =
|
||||||
|
account.address.getEscapedLocal()
|
||||||
|
+ ":"
|
||||||
|
+ account.address.getDomain()
|
||||||
|
+ ":"
|
||||||
|
+ credential.password;
|
||||||
|
final MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
final byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
|
||||||
|
final String cNonce = CryptoHelper.random(100);
|
||||||
|
final byte[] a1 =
|
||||||
|
CryptoHelper.concatenateByteArrays(
|
||||||
|
y,
|
||||||
|
(":" + nonce + ":" + cNonce)
|
||||||
|
.getBytes(Charset.defaultCharset()));
|
||||||
|
final String a2 = "AUTHENTICATE:" + digestUri;
|
||||||
|
final String ha1 = CryptoHelper.bytesToHex(md.digest(a1));
|
||||||
|
final String ha2 =
|
||||||
|
CryptoHelper.bytesToHex(
|
||||||
|
md.digest(a2.getBytes(Charset.defaultCharset())));
|
||||||
|
final String kd =
|
||||||
|
ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + ":auth:" + ha2;
|
||||||
|
final String response =
|
||||||
|
CryptoHelper.bytesToHex(
|
||||||
|
md.digest(kd.getBytes(Charset.defaultCharset())));
|
||||||
|
final String saslString =
|
||||||
|
"username=\""
|
||||||
|
+ account.address.getEscapedLocal()
|
||||||
|
+ "\",realm=\""
|
||||||
|
+ account.address.getDomain()
|
||||||
|
+ "\",nonce=\""
|
||||||
|
+ nonce
|
||||||
|
+ "\",cnonce=\""
|
||||||
|
+ cNonce
|
||||||
|
+ "\",nc="
|
||||||
|
+ nonceCount
|
||||||
|
+ ",qop=auth,digest-uri=\""
|
||||||
|
+ digestUri
|
||||||
|
+ "\",response="
|
||||||
|
+ response
|
||||||
|
+ ",charset=utf-8";
|
||||||
|
encodedResponse =
|
||||||
|
Base64.encodeToString(
|
||||||
|
saslString.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
|
||||||
|
} catch (final NoSuchAlgorithmException e) {
|
||||||
|
throw new AuthenticationException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodedResponse;
|
||||||
|
case RESPONSE_SENT:
|
||||||
|
state = State.VALID_SERVER_RESPONSE;
|
||||||
|
break;
|
||||||
|
case VALID_SERVER_RESPONSE:
|
||||||
|
if (challenge == null) {
|
||||||
|
return null; // everything is fine
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new InvalidStateException(state);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
public class External extends SaslMechanism {
|
||||||
|
|
||||||
|
public static final String MECHANISM = "EXTERNAL";
|
||||||
|
|
||||||
|
public External(final Account account) {
|
||||||
|
super(account, Credential.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
|
return Base64.encodeToString(account.address.toEscapedString().getBytes(), Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
import android.util.Log;
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.ImmutableMultimap;
|
||||||
|
import com.google.common.collect.Multimap;
|
||||||
|
import com.google.common.hash.HashFunction;
|
||||||
|
import com.google.common.primitives.Bytes;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.utils.SSLSockets;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public abstract class HashedToken extends SaslMechanism implements ChannelBindingMechanism {
|
||||||
|
|
||||||
|
private static final String PREFIX = "HT";
|
||||||
|
|
||||||
|
private static final List<String> HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256");
|
||||||
|
private static final byte[] INITIATOR = "Initiator".getBytes(StandardCharsets.UTF_8);
|
||||||
|
private static final byte[] RESPONDER = "Responder".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
protected final ChannelBinding channelBinding;
|
||||||
|
|
||||||
|
protected HashedToken(
|
||||||
|
final Account account,
|
||||||
|
final Credential credential,
|
||||||
|
final ChannelBinding channelBinding) {
|
||||||
|
super(account, credential);
|
||||||
|
this.channelBinding = channelBinding;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
|
final String token = Strings.nullToEmpty(this.credential.fastToken);
|
||||||
|
final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8));
|
||||||
|
final byte[] cbData = getChannelBindingData(sslSocket);
|
||||||
|
final byte[] initiatorHashedToken =
|
||||||
|
hashing.hashBytes(Bytes.concat(INITIATOR, cbData)).asBytes();
|
||||||
|
final byte[] firstMessage =
|
||||||
|
Bytes.concat(
|
||||||
|
account.address.getEscapedLocal().getBytes(StandardCharsets.UTF_8),
|
||||||
|
new byte[] {0x00},
|
||||||
|
initiatorHashedToken);
|
||||||
|
return Base64.encodeToString(firstMessage, Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getChannelBindingData(final SSLSocket sslSocket) {
|
||||||
|
if (this.channelBinding == ChannelBinding.NONE) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding);
|
||||||
|
} catch (final AuthenticationException e) {
|
||||||
|
Log.e(
|
||||||
|
Config.LOGTAG,
|
||||||
|
account.address
|
||||||
|
+ ": unable to retrieve channel binding data for "
|
||||||
|
+ getMechanism(),
|
||||||
|
e);
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getResponse(final String challenge, final SSLSocket socket)
|
||||||
|
throws AuthenticationException {
|
||||||
|
final byte[] responderMessage;
|
||||||
|
try {
|
||||||
|
responderMessage = Base64.decode(challenge, Base64.NO_WRAP);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new AuthenticationException("Unable to decode responder message", e);
|
||||||
|
}
|
||||||
|
final String token = Strings.nullToEmpty(this.credential.fastToken);
|
||||||
|
final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8));
|
||||||
|
final byte[] cbData = getChannelBindingData(socket);
|
||||||
|
final byte[] expectedResponderMessage =
|
||||||
|
hashing.hashBytes(Bytes.concat(RESPONDER, cbData)).asBytes();
|
||||||
|
if (Arrays.equals(responderMessage, expectedResponderMessage)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new AuthenticationException("Responder message did not match");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract HashFunction getHashFunction(final byte[] key);
|
||||||
|
|
||||||
|
public abstract Mechanism getTokenMechanism();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return getTokenMechanism().name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Mechanism {
|
||||||
|
public final String hashFunction;
|
||||||
|
public final ChannelBinding channelBinding;
|
||||||
|
|
||||||
|
public Mechanism(String hashFunction, ChannelBinding channelBinding) {
|
||||||
|
this.hashFunction = hashFunction;
|
||||||
|
this.channelBinding = channelBinding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Mechanism of(final String mechanism) {
|
||||||
|
final int first = mechanism.indexOf('-');
|
||||||
|
final int last = mechanism.lastIndexOf('-');
|
||||||
|
if (last <= first || mechanism.length() <= last) {
|
||||||
|
throw new IllegalArgumentException("Not a valid HashedToken name");
|
||||||
|
}
|
||||||
|
if (mechanism.substring(0, first).equals(PREFIX)) {
|
||||||
|
final String hashFunction = mechanism.substring(first + 1, last);
|
||||||
|
final String cbShortName = mechanism.substring(last + 1);
|
||||||
|
final ChannelBinding channelBinding =
|
||||||
|
ChannelBinding.SHORT_NAMES.inverse().get(cbShortName);
|
||||||
|
if (channelBinding == null) {
|
||||||
|
throw new IllegalArgumentException("Unknown channel binding " + cbShortName);
|
||||||
|
}
|
||||||
|
return new Mechanism(hashFunction, channelBinding);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("HashedToken name does not start with HT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Mechanism ofOrNull(final String mechanism) {
|
||||||
|
try {
|
||||||
|
return mechanism == null ? null : of(mechanism);
|
||||||
|
} catch (final IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Multimap<String, ChannelBinding> of(final Collection<String> mechanisms) {
|
||||||
|
final ImmutableMultimap.Builder<String, ChannelBinding> builder =
|
||||||
|
ImmutableMultimap.builder();
|
||||||
|
for (final String name : mechanisms) {
|
||||||
|
try {
|
||||||
|
final Mechanism mechanism = Mechanism.of(name);
|
||||||
|
builder.put(mechanism.hashFunction, mechanism.channelBinding);
|
||||||
|
} catch (final IllegalArgumentException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Mechanism best(
|
||||||
|
final Collection<String> mechanisms, final SSLSockets.Version sslVersion) {
|
||||||
|
final Multimap<String, ChannelBinding> multimap = of(mechanisms);
|
||||||
|
for (final String hashFunction : HASH_FUNCTIONS) {
|
||||||
|
final Collection<ChannelBinding> channelBindings = multimap.get(hashFunction);
|
||||||
|
if (channelBindings.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final ChannelBinding cb = ChannelBinding.best(channelBindings, sslVersion);
|
||||||
|
return new Mechanism(hashFunction, cb);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this)
|
||||||
|
.add("hashFunction", hashFunction)
|
||||||
|
.add("channelBinding", channelBinding)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String name() {
|
||||||
|
return String.format(
|
||||||
|
"%s-%s-%s",
|
||||||
|
PREFIX, hashFunction, ChannelBinding.SHORT_NAMES.get(channelBinding));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChannelBinding getChannelBinding() {
|
||||||
|
return this.channelBinding;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashFunction;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
|
||||||
|
public class HashedTokenSha256 extends HashedToken {
|
||||||
|
|
||||||
|
public HashedTokenSha256(
|
||||||
|
final Account account,
|
||||||
|
final Credential credential,
|
||||||
|
final ChannelBinding channelBinding) {
|
||||||
|
super(account, credential, channelBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getHashFunction(final byte[] key) {
|
||||||
|
return Hashing.hmacSha256(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mechanism getTokenMechanism() {
|
||||||
|
return new Mechanism("SHA-256", channelBinding);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashFunction;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
|
||||||
|
public class HashedTokenSha512 extends HashedToken {
|
||||||
|
|
||||||
|
public HashedTokenSha512(
|
||||||
|
final Account account,
|
||||||
|
final Credential credential,
|
||||||
|
final ChannelBinding channelBinding) {
|
||||||
|
super(account, credential, channelBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getHashFunction(final byte[] key) {
|
||||||
|
return Hashing.hmacSha512(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mechanism getTokenMechanism() {
|
||||||
|
return new Mechanism("SHA-512", this.channelBinding);
|
||||||
|
}
|
||||||
|
}
|
36
src/main/java/im/conversations/android/xmpp/sasl/Plain.java
Normal file
36
src/main/java/im/conversations/android/xmpp/sasl/Plain.java
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
public class Plain extends SaslMechanism {
|
||||||
|
|
||||||
|
public static final String MECHANISM = "PLAIN";
|
||||||
|
|
||||||
|
public Plain(final Account account, final Credential credential) {
|
||||||
|
super(account, credential);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getMessage(String username, String password) {
|
||||||
|
final String message = '\u0000' + username + '\u0000' + password;
|
||||||
|
return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
|
return getMessage(account.address.getEscapedLocal(), credential.password);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,236 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.Collections2;
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
|
import eu.siacs.conversations.utils.SSLSockets;
|
||||||
|
import eu.siacs.conversations.xml.Element;
|
||||||
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
public abstract class SaslMechanism {
|
||||||
|
|
||||||
|
protected final Account account;
|
||||||
|
protected final Credential credential;
|
||||||
|
|
||||||
|
protected SaslMechanism(final Account account, final Credential credential) {
|
||||||
|
this.account = account;
|
||||||
|
this.credential = credential;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String namespace(final Version version) {
|
||||||
|
if (version == Version.SASL) {
|
||||||
|
return Namespace.SASL;
|
||||||
|
} else {
|
||||||
|
return Namespace.SASL_2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The priority is used to pin the authentication mechanism. If authentication fails, it MAY be
|
||||||
|
* retried with another mechanism of the same priority, but MUST NOT be tried with a mechanism
|
||||||
|
* of lower priority (to prevent downgrade attacks).
|
||||||
|
*
|
||||||
|
* @return An arbitrary int representing the priority
|
||||||
|
*/
|
||||||
|
public abstract int getPriority();
|
||||||
|
|
||||||
|
public abstract String getMechanism();
|
||||||
|
|
||||||
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResponse(final String challenge, final SSLSocket sslSocket)
|
||||||
|
throws AuthenticationException {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Collection<String> mechanisms(final Element authElement) {
|
||||||
|
if (authElement == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return Collections2.transform(
|
||||||
|
Collections2.filter(
|
||||||
|
authElement.getChildren(),
|
||||||
|
c -> c != null && "mechanism".equals(c.getName())),
|
||||||
|
c -> c == null ? null : c.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected enum State {
|
||||||
|
INITIAL,
|
||||||
|
AUTH_TEXT_SENT,
|
||||||
|
RESPONSE_SENT,
|
||||||
|
VALID_SERVER_RESPONSE,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Version {
|
||||||
|
SASL,
|
||||||
|
SASL_2;
|
||||||
|
|
||||||
|
public static Version of(final Element element) {
|
||||||
|
switch (Strings.nullToEmpty(element.getNamespace())) {
|
||||||
|
case Namespace.SASL:
|
||||||
|
return SASL;
|
||||||
|
case Namespace.SASL_2:
|
||||||
|
return SASL_2;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Unrecognized SASL namespace");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AuthenticationException extends Exception {
|
||||||
|
public AuthenticationException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthenticationException(final Exception inner) {
|
||||||
|
super(inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthenticationException(final String message, final Exception exception) {
|
||||||
|
super(message, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class InvalidStateException extends AuthenticationException {
|
||||||
|
public InvalidStateException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidStateException(final State state) {
|
||||||
|
this("Invalid state: " + state.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Factory {
|
||||||
|
|
||||||
|
private final Account account;
|
||||||
|
private final Credential credential;
|
||||||
|
|
||||||
|
public Factory(final Account account, final Credential credential) {
|
||||||
|
this.account = account;
|
||||||
|
this.credential = credential;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SaslMechanism of(
|
||||||
|
final Collection<String> mechanisms, final ChannelBinding channelBinding) {
|
||||||
|
Preconditions.checkNotNull(channelBinding, "Use ChannelBinding.NONE instead of null");
|
||||||
|
if (mechanisms.contains(External.MECHANISM) && credential.privateKeyAlias != null) {
|
||||||
|
return new External(account);
|
||||||
|
} else if (mechanisms.contains(ScramSha512Plus.MECHANISM)
|
||||||
|
&& channelBinding != ChannelBinding.NONE) {
|
||||||
|
return new ScramSha512Plus(account, credential, channelBinding);
|
||||||
|
} else if (mechanisms.contains(ScramSha256Plus.MECHANISM)
|
||||||
|
&& channelBinding != ChannelBinding.NONE) {
|
||||||
|
return new ScramSha256Plus(account, credential, channelBinding);
|
||||||
|
} else if (mechanisms.contains(ScramSha1Plus.MECHANISM)
|
||||||
|
&& channelBinding != ChannelBinding.NONE) {
|
||||||
|
return new ScramSha1Plus(account, credential, channelBinding);
|
||||||
|
} else if (mechanisms.contains(ScramSha512.MECHANISM)) {
|
||||||
|
return new ScramSha512(account, credential);
|
||||||
|
} else if (mechanisms.contains(ScramSha256.MECHANISM)) {
|
||||||
|
return new ScramSha256(account, credential);
|
||||||
|
} else if (mechanisms.contains(ScramSha1.MECHANISM)) {
|
||||||
|
return new ScramSha1(account, credential);
|
||||||
|
} else if (mechanisms.contains(Plain.MECHANISM)) {
|
||||||
|
return new Plain(account, credential);
|
||||||
|
} else if (mechanisms.contains(DigestMd5.MECHANISM)) {
|
||||||
|
return new DigestMd5(account, credential);
|
||||||
|
} else if (mechanisms.contains(Anonymous.MECHANISM)) {
|
||||||
|
return new Anonymous(account);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SaslMechanism of(
|
||||||
|
final Collection<String> mechanisms,
|
||||||
|
final Collection<ChannelBinding> bindings,
|
||||||
|
final Version version,
|
||||||
|
final SSLSockets.Version sslVersion) {
|
||||||
|
final HashedToken fastMechanism = getFastMechanism();
|
||||||
|
if (version == Version.SASL_2 && fastMechanism != null) {
|
||||||
|
return fastMechanism;
|
||||||
|
}
|
||||||
|
final ChannelBinding channelBinding = ChannelBinding.best(bindings, sslVersion);
|
||||||
|
return of(mechanisms, channelBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SaslMechanism of(final String mechanism, final ChannelBinding channelBinding) {
|
||||||
|
return of(Collections.singleton(mechanism), channelBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashedToken getFastMechanism() {
|
||||||
|
final HashedToken.Mechanism fastMechanism =
|
||||||
|
HashedToken.Mechanism.ofOrNull(credential.fastMechanism);
|
||||||
|
final String token = credential.fastToken;
|
||||||
|
if (fastMechanism == null || Strings.isNullOrEmpty(token)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (fastMechanism.hashFunction.equals("SHA-256")) {
|
||||||
|
return new HashedTokenSha256(account, credential, fastMechanism.channelBinding);
|
||||||
|
} else if (fastMechanism.hashFunction.equals("SHA-512")) {
|
||||||
|
return new HashedTokenSha512(account, credential, fastMechanism.channelBinding);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SaslMechanism getPinnedMechanism() {
|
||||||
|
final String mechanism = Strings.nullToEmpty(credential.pinnedMechanism);
|
||||||
|
final ChannelBinding channelBinding =
|
||||||
|
ChannelBinding.get(credential.pinnedChannelBinding);
|
||||||
|
return this.of(mechanism, channelBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SaslMechanism getQuickStartMechanism() {
|
||||||
|
final HashedToken hashedTokenMechanism = getFastMechanism();
|
||||||
|
if (hashedTokenMechanism != null) {
|
||||||
|
return hashedTokenMechanism;
|
||||||
|
}
|
||||||
|
return getPinnedMechanism();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPinnedMechanismPriority() {
|
||||||
|
final SaslMechanism saslMechanism = getPinnedMechanism();
|
||||||
|
if (saslMechanism == null) {
|
||||||
|
return Integer.MIN_VALUE;
|
||||||
|
} else {
|
||||||
|
return saslMechanism.getPriority();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SaslMechanism ensureAvailable(
|
||||||
|
final SaslMechanism mechanism, final SSLSockets.Version sslVersion) {
|
||||||
|
if (mechanism instanceof ChannelBindingMechanism) {
|
||||||
|
final ChannelBinding cb = ((ChannelBindingMechanism) mechanism).getChannelBinding();
|
||||||
|
if (ChannelBinding.isAvailable(cb, sslVersion)) {
|
||||||
|
return mechanism;
|
||||||
|
} else {
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
"pinned channel binding method " + cb + " no longer available");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return mechanism;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hashedToken(final SaslMechanism saslMechanism) {
|
||||||
|
return saslMechanism instanceof HashedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean pin(final SaslMechanism saslMechanism) {
|
||||||
|
return !hashedToken(saslMechanism);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,318 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
import com.google.common.base.CaseFormat;
|
||||||
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.common.cache.CacheBuilder;
|
||||||
|
import com.google.common.hash.HashFunction;
|
||||||
|
import eu.siacs.conversations.utils.CryptoHelper;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
abstract class ScramMechanism extends SaslMechanism {
|
||||||
|
|
||||||
|
public static final SecretKey EMPTY_KEY =
|
||||||
|
new SecretKey() {
|
||||||
|
@Override
|
||||||
|
public String getAlgorithm() {
|
||||||
|
return "HMAC";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFormat() {
|
||||||
|
return "RAW";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getEncoded() {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
|
||||||
|
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
|
||||||
|
private static final Cache<CacheKey, KeyPair> CACHE =
|
||||||
|
CacheBuilder.newBuilder().maximumSize(10).build();
|
||||||
|
protected final ChannelBinding channelBinding;
|
||||||
|
private final String gs2Header;
|
||||||
|
private final String clientNonce;
|
||||||
|
protected State state = State.INITIAL;
|
||||||
|
private String clientFirstMessageBare;
|
||||||
|
private byte[] serverSignature = null;
|
||||||
|
|
||||||
|
ScramMechanism(
|
||||||
|
final Account account,
|
||||||
|
final Credential credential,
|
||||||
|
final ChannelBinding channelBinding) {
|
||||||
|
super(account, credential);
|
||||||
|
this.channelBinding = channelBinding;
|
||||||
|
if (channelBinding == ChannelBinding.NONE) {
|
||||||
|
// TODO this needs to be changed to "y,," for the scram internal down grade protection
|
||||||
|
// but we might risk compatibility issues if the server supports a binding that we don’t
|
||||||
|
// support
|
||||||
|
this.gs2Header = "n,,";
|
||||||
|
} else {
|
||||||
|
this.gs2Header =
|
||||||
|
String.format(
|
||||||
|
"p=%s,,",
|
||||||
|
CaseFormat.UPPER_UNDERSCORE
|
||||||
|
.converterTo(CaseFormat.LOWER_HYPHEN)
|
||||||
|
.convert(channelBinding.toString()));
|
||||||
|
}
|
||||||
|
// This nonce should be different for each authentication attempt.
|
||||||
|
this.clientNonce = CryptoHelper.random(100);
|
||||||
|
clientFirstMessageBare = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract HashFunction getHMac(final byte[] key);
|
||||||
|
|
||||||
|
protected abstract HashFunction getDigest();
|
||||||
|
|
||||||
|
private KeyPair getKeyPair(final String password, final String salt, final int iterations)
|
||||||
|
throws ExecutionException {
|
||||||
|
return CACHE.get(
|
||||||
|
new CacheKey(getMechanism(), password, salt, iterations),
|
||||||
|
() -> {
|
||||||
|
final byte[] saltedPassword, serverKey, clientKey;
|
||||||
|
saltedPassword =
|
||||||
|
hi(
|
||||||
|
password.getBytes(),
|
||||||
|
Base64.decode(salt, Base64.DEFAULT),
|
||||||
|
iterations);
|
||||||
|
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
|
||||||
|
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
|
||||||
|
return new KeyPair(clientKey, serverKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException {
|
||||||
|
return getHMac(key).hashBytes(input).asBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] digest(final byte[] bytes) {
|
||||||
|
return getDigest().hashBytes(bytes).asBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the
|
||||||
|
* pseudorandom function (PRF) and with dkLen == output length of
|
||||||
|
* HMAC() == output length of H().
|
||||||
|
*/
|
||||||
|
private byte[] hi(final byte[] key, final byte[] salt, final int iterations)
|
||||||
|
throws InvalidKeyException {
|
||||||
|
byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE));
|
||||||
|
byte[] out = u.clone();
|
||||||
|
for (int i = 1; i < iterations; i++) {
|
||||||
|
u = hmac(key, u);
|
||||||
|
for (int j = 0; j < u.length; j++) {
|
||||||
|
out[j] ^= u[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
|
if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
|
||||||
|
clientFirstMessageBare =
|
||||||
|
"n="
|
||||||
|
+ CryptoHelper.saslEscape(
|
||||||
|
CryptoHelper.saslPrep(account.address.getEscapedLocal()))
|
||||||
|
+ ",r="
|
||||||
|
+ this.clientNonce;
|
||||||
|
state = State.AUTH_TEXT_SENT;
|
||||||
|
}
|
||||||
|
return Base64.encodeToString(
|
||||||
|
(gs2Header + clientFirstMessageBare).getBytes(Charset.defaultCharset()),
|
||||||
|
Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getResponse(final String challenge, final SSLSocket socket)
|
||||||
|
throws AuthenticationException {
|
||||||
|
switch (state) {
|
||||||
|
case AUTH_TEXT_SENT:
|
||||||
|
if (challenge == null) {
|
||||||
|
throw new AuthenticationException("challenge can not be null");
|
||||||
|
}
|
||||||
|
byte[] serverFirstMessage;
|
||||||
|
try {
|
||||||
|
serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new AuthenticationException("Unable to decode server challenge", e);
|
||||||
|
}
|
||||||
|
final Tokenizer tokenizer = new Tokenizer(serverFirstMessage);
|
||||||
|
String nonce = "";
|
||||||
|
int iterationCount = -1;
|
||||||
|
String salt = "";
|
||||||
|
for (final String token : tokenizer) {
|
||||||
|
if (token.charAt(1) == '=') {
|
||||||
|
switch (token.charAt(0)) {
|
||||||
|
case 'i':
|
||||||
|
try {
|
||||||
|
iterationCount = Integer.parseInt(token.substring(2));
|
||||||
|
} catch (final NumberFormatException e) {
|
||||||
|
throw new AuthenticationException(e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
salt = token.substring(2);
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
nonce = token.substring(2);
|
||||||
|
break;
|
||||||
|
case 'm':
|
||||||
|
/*
|
||||||
|
* RFC 5802:
|
||||||
|
* m: This attribute is reserved for future extensibility. In this
|
||||||
|
* version of SCRAM, its presence in a client or a server message
|
||||||
|
* MUST cause authentication failure when the attribute is parsed by
|
||||||
|
* the other end.
|
||||||
|
*/
|
||||||
|
throw new AuthenticationException(
|
||||||
|
"Server sent reserved token: `m'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iterationCount < 0) {
|
||||||
|
throw new AuthenticationException("Server did not send iteration count");
|
||||||
|
}
|
||||||
|
if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) {
|
||||||
|
throw new AuthenticationException(
|
||||||
|
"Server nonce does not contain client nonce: " + nonce);
|
||||||
|
}
|
||||||
|
if (salt.isEmpty()) {
|
||||||
|
throw new AuthenticationException("Server sent empty salt");
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] channelBindingData = getChannelBindingData(socket);
|
||||||
|
|
||||||
|
final int gs2Len = this.gs2Header.getBytes().length;
|
||||||
|
final byte[] cMessage = new byte[gs2Len + channelBindingData.length];
|
||||||
|
System.arraycopy(this.gs2Header.getBytes(), 0, cMessage, 0, gs2Len);
|
||||||
|
System.arraycopy(
|
||||||
|
channelBindingData, 0, cMessage, gs2Len, channelBindingData.length);
|
||||||
|
|
||||||
|
final String clientFinalMessageWithoutProof =
|
||||||
|
"c=" + Base64.encodeToString(cMessage, Base64.NO_WRAP) + ",r=" + nonce;
|
||||||
|
|
||||||
|
final byte[] authMessage =
|
||||||
|
(clientFirstMessageBare
|
||||||
|
+ ','
|
||||||
|
+ new String(serverFirstMessage)
|
||||||
|
+ ','
|
||||||
|
+ clientFinalMessageWithoutProof)
|
||||||
|
.getBytes();
|
||||||
|
|
||||||
|
final KeyPair keys;
|
||||||
|
try {
|
||||||
|
keys =
|
||||||
|
getKeyPair(
|
||||||
|
CryptoHelper.saslPrep(credential.password),
|
||||||
|
salt,
|
||||||
|
iterationCount);
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
throw new AuthenticationException("Invalid keys generated");
|
||||||
|
}
|
||||||
|
final byte[] clientSignature;
|
||||||
|
try {
|
||||||
|
serverSignature = hmac(keys.serverKey, authMessage);
|
||||||
|
final byte[] storedKey = digest(keys.clientKey);
|
||||||
|
|
||||||
|
clientSignature = hmac(storedKey, authMessage);
|
||||||
|
|
||||||
|
} catch (final InvalidKeyException e) {
|
||||||
|
throw new AuthenticationException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] clientProof = new byte[keys.clientKey.length];
|
||||||
|
|
||||||
|
if (clientSignature.length < keys.clientKey.length) {
|
||||||
|
throw new AuthenticationException(
|
||||||
|
"client signature was shorter than clientKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < clientProof.length; i++) {
|
||||||
|
clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String clientFinalMessage =
|
||||||
|
clientFinalMessageWithoutProof
|
||||||
|
+ ",p="
|
||||||
|
+ Base64.encodeToString(clientProof, Base64.NO_WRAP);
|
||||||
|
state = State.RESPONSE_SENT;
|
||||||
|
return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP);
|
||||||
|
case RESPONSE_SENT:
|
||||||
|
try {
|
||||||
|
final String clientCalculatedServerFinalMessage =
|
||||||
|
"v=" + Base64.encodeToString(serverSignature, Base64.NO_WRAP);
|
||||||
|
if (!clientCalculatedServerFinalMessage.equals(
|
||||||
|
new String(Base64.decode(challenge, Base64.DEFAULT)))) {
|
||||||
|
throw new Exception();
|
||||||
|
}
|
||||||
|
state = State.VALID_SERVER_RESPONSE;
|
||||||
|
return "";
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AuthenticationException(
|
||||||
|
"Server final message does not match calculated final message");
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new InvalidStateException(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected byte[] getChannelBindingData(final SSLSocket sslSocket)
|
||||||
|
throws AuthenticationException {
|
||||||
|
if (this.channelBinding == ChannelBinding.NONE) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
throw new AssertionError("getChannelBindingData needs to be overwritten");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CacheKey {
|
||||||
|
final String algorithm;
|
||||||
|
final String password;
|
||||||
|
final String salt;
|
||||||
|
final int iterations;
|
||||||
|
|
||||||
|
private CacheKey(String algorithm, String password, String salt, int iterations) {
|
||||||
|
this.algorithm = algorithm;
|
||||||
|
this.password = password;
|
||||||
|
this.salt = salt;
|
||||||
|
this.iterations = iterations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
CacheKey cacheKey = (CacheKey) o;
|
||||||
|
return iterations == cacheKey.iterations
|
||||||
|
&& Objects.equal(algorithm, cacheKey.algorithm)
|
||||||
|
&& Objects.equal(password, cacheKey.password)
|
||||||
|
&& Objects.equal(salt, cacheKey.salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(algorithm, password, salt, iterations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class KeyPair {
|
||||||
|
final byte[] clientKey;
|
||||||
|
final byte[] serverKey;
|
||||||
|
|
||||||
|
KeyPair(final byte[] clientKey, final byte[] serverKey) {
|
||||||
|
this.clientKey = clientKey;
|
||||||
|
this.serverKey = serverKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
public abstract class ScramPlusMechanism extends ScramMechanism implements ChannelBindingMechanism {
|
||||||
|
|
||||||
|
ScramPlusMechanism(
|
||||||
|
Account account, final Credential credential, ChannelBinding channelBinding) {
|
||||||
|
super(account, credential, channelBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected byte[] getChannelBindingData(final SSLSocket sslSocket)
|
||||||
|
throws AuthenticationException {
|
||||||
|
return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ChannelBinding getChannelBinding() {
|
||||||
|
return this.channelBinding;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashFunction;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
|
||||||
|
public class ScramSha1 extends ScramMechanism {
|
||||||
|
|
||||||
|
public static final String MECHANISM = "SCRAM-SHA-1";
|
||||||
|
|
||||||
|
public ScramSha1(final Account account, final Credential credential) {
|
||||||
|
super(account, credential, ChannelBinding.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getHMac(final byte[] key) {
|
||||||
|
return (key == null || key.length == 0)
|
||||||
|
? Hashing.hmacSha1(EMPTY_KEY)
|
||||||
|
: Hashing.hmacSha1(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getDigest() {
|
||||||
|
return Hashing.sha1();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashFunction;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
|
||||||
|
public class ScramSha1Plus extends ScramPlusMechanism {
|
||||||
|
|
||||||
|
public static final String MECHANISM = "SCRAM-SHA-1-PLUS";
|
||||||
|
|
||||||
|
public ScramSha1Plus(
|
||||||
|
final Account account, Credential credential, final ChannelBinding channelBinding) {
|
||||||
|
super(account, credential, channelBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getHMac(final byte[] key) {
|
||||||
|
return (key == null || key.length == 0)
|
||||||
|
? Hashing.hmacSha1(EMPTY_KEY)
|
||||||
|
: Hashing.hmacSha1(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getDigest() {
|
||||||
|
return Hashing.sha1();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 35; // higher than SCRAM-SHA512 (30)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashFunction;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
|
||||||
|
public class ScramSha256 extends ScramMechanism {
|
||||||
|
|
||||||
|
public static final String MECHANISM = "SCRAM-SHA-256";
|
||||||
|
|
||||||
|
public ScramSha256(final Account account, final Credential credential) {
|
||||||
|
super(account, credential, ChannelBinding.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getHMac(final byte[] key) {
|
||||||
|
return (key == null || key.length == 0)
|
||||||
|
? Hashing.hmacSha256(EMPTY_KEY)
|
||||||
|
: Hashing.hmacSha256(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getDigest() {
|
||||||
|
return Hashing.sha256();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashFunction;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
|
||||||
|
public class ScramSha256Plus extends ScramPlusMechanism {
|
||||||
|
|
||||||
|
public static final String MECHANISM = "SCRAM-SHA-256-PLUS";
|
||||||
|
|
||||||
|
public ScramSha256Plus(
|
||||||
|
final Account account,
|
||||||
|
final Credential credential,
|
||||||
|
final ChannelBinding channelBinding) {
|
||||||
|
super(account, credential, channelBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getHMac(final byte[] key) {
|
||||||
|
return (key == null || key.length == 0)
|
||||||
|
? Hashing.hmacSha256(EMPTY_KEY)
|
||||||
|
: Hashing.hmacSha256(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getDigest() {
|
||||||
|
return Hashing.sha256();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashFunction;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
|
||||||
|
public class ScramSha512 extends ScramMechanism {
|
||||||
|
|
||||||
|
public static final String MECHANISM = "SCRAM-SHA-512";
|
||||||
|
|
||||||
|
public ScramSha512(final Account account, final Credential credential) {
|
||||||
|
super(account, credential, ChannelBinding.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getHMac(final byte[] key) {
|
||||||
|
return (key == null || key.length == 0)
|
||||||
|
? Hashing.hmacSha512(EMPTY_KEY)
|
||||||
|
: Hashing.hmacSha512(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getDigest() {
|
||||||
|
return Hashing.sha512();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashFunction;
|
||||||
|
import com.google.common.hash.Hashing;
|
||||||
|
import im.conversations.android.database.model.Account;
|
||||||
|
import im.conversations.android.database.model.Credential;
|
||||||
|
|
||||||
|
public class ScramSha512Plus extends ScramPlusMechanism {
|
||||||
|
|
||||||
|
public static final String MECHANISM = "SCRAM-SHA-512-PLUS";
|
||||||
|
|
||||||
|
public ScramSha512Plus(
|
||||||
|
final Account account,
|
||||||
|
final Credential credential,
|
||||||
|
final ChannelBinding channelBinding) {
|
||||||
|
super(account, credential, channelBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getHMac(final byte[] key) {
|
||||||
|
return (key == null || key.length == 0)
|
||||||
|
? Hashing.hmacSha512(EMPTY_KEY)
|
||||||
|
: Hashing.hmacSha512(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HashFunction getDigest() {
|
||||||
|
return Hashing.sha512();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPriority() {
|
||||||
|
return 45;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMechanism() {
|
||||||
|
return MECHANISM;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package im.conversations.android.xmpp.sasl;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
/** A tokenizer for GS2 header strings */
|
||||||
|
public final class Tokenizer implements Iterator<String>, Iterable<String> {
|
||||||
|
private final List<String> parts;
|
||||||
|
private int index;
|
||||||
|
|
||||||
|
public Tokenizer(final byte[] challenge) {
|
||||||
|
final String challengeString = new String(challenge);
|
||||||
|
parts = new ArrayList<>(Arrays.asList(challengeString.split(",")));
|
||||||
|
// Trim parts.
|
||||||
|
for (int i = 0; i < parts.size(); i++) {
|
||||||
|
parts.set(i, parts.get(i).trim());
|
||||||
|
}
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there is at least one more element, false otherwise.
|
||||||
|
*
|
||||||
|
* @see #next
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean hasNext() {
|
||||||
|
return parts.size() != index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next object and advances the iterator.
|
||||||
|
*
|
||||||
|
* @return the next object.
|
||||||
|
* @throws java.util.NoSuchElementException if there are no more elements.
|
||||||
|
* @see #hasNext
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String next() {
|
||||||
|
if (hasNext()) {
|
||||||
|
return parts.get(index++);
|
||||||
|
} else {
|
||||||
|
throw new NoSuchElementException("No such element. Size is: " + parts.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the last object returned by {@code next} from the collection. This method can only be
|
||||||
|
* called once between each call to {@code next}.
|
||||||
|
*
|
||||||
|
* @throws UnsupportedOperationException if removing is not supported by the collection being
|
||||||
|
* iterated.
|
||||||
|
* @throws IllegalStateException if {@code next} has not been called, or {@code remove} has
|
||||||
|
* already been called after the last call to {@code next}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void remove() {
|
||||||
|
if (index <= 0) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"You can't delete an element before first next() method call");
|
||||||
|
}
|
||||||
|
parts.remove(--index);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an {@link java.util.Iterator} for the elements in this object.
|
||||||
|
*
|
||||||
|
* @return An {@code Iterator} instance.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Iterator<String> iterator() {
|
||||||
|
return parts.iterator();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue