do quick start with HT-SHA-256-NONE

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/Config.java                              |   2 
src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java          |   2 
src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBindingMechanism.java |   6 
src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java             |  57 
src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java       |   5 
src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java       |   5 
src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java           |   6 
src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java      |   3 
src/main/java/eu/siacs/conversations/entities/Account.java                    | 144 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java         |   9 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java                 |  60 
11 files changed, 237 insertions(+), 62 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/Config.java 🔗

@@ -60,7 +60,7 @@ public final class Config {
     public static final long CONTACT_SYNC_RETRY_INTERVAL = 1000L * 60 * 5;
 
 
-    public static final boolean QUICKSTART_ENABLED = false;
+    public static final boolean QUICKSTART_ENABLED = true;
 
     //Notification settings
     public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false;

src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java 🔗

@@ -97,7 +97,7 @@ public enum ChannelBinding {
         }
     }
 
-    public static boolean ensureBest(
+    public static boolean isAvailable(
             final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) {
         return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion)
                 == channelBinding;

src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java 🔗

@@ -1,12 +1,17 @@
 package eu.siacs.conversations.crypto.sasl;
 
+import android.util.Base64;
+
 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 org.jetbrains.annotations.NotNull;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
@@ -16,11 +21,13 @@ import javax.net.ssl.SSLSocket;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.utils.SSLSockets;
 
-public abstract class HashedToken extends SaslMechanism {
+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;
 
@@ -36,18 +43,48 @@ public abstract class HashedToken extends SaslMechanism {
 
     @Override
     public String getClientFirstMessage() {
-        return null; // HMAC(token, "Initiator" || cb-data)
+        final String token = Strings.nullToEmpty(this.account.getFastToken());
+        final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8));
+        final byte[] cbData = new byte[0];
+        final byte[] initiatorHashedToken =
+                hashing.hashBytes(Bytes.concat(INITIATOR, cbData)).asBytes();
+        final byte[] firstMessage =
+                Bytes.concat(
+                        account.getUsername().getBytes(StandardCharsets.UTF_8),
+                        new byte[] {0x00},
+                        initiatorHashedToken);
+        return Base64.encodeToString(firstMessage, Base64.NO_WRAP);
     }
 
     @Override
     public String getResponse(final String challenge, final SSLSocket socket)
             throws AuthenticationException {
-        // todo verify that challenge matches HMAC(token, "Responder" || cb-data)
-        return null;
+        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.account.getFastToken());
+        final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8));
+        final byte[] cbData = new byte[0];
+        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;
@@ -77,6 +114,14 @@ public abstract class HashedToken extends SaslMechanism {
             }
         }
 
+        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();
@@ -119,4 +164,8 @@ public abstract class HashedToken extends SaslMechanism {
                     PREFIX, hashFunction, ChannelBinding.SHORT_NAMES.get(channelBinding));
         }
     }
+
+    public ChannelBinding getChannelBinding() {
+        return this.channelBinding;
+    }
 }

src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java 🔗

@@ -17,8 +17,7 @@ public class HashedTokenSha256 extends HashedToken {
     }
 
     @Override
-    public String getMechanism() {
-        final String cbShortName = ChannelBinding.SHORT_NAMES.get(this.channelBinding);
-        return String.format("HT-SHA-256-%s", cbShortName);
+    public Mechanism getTokenMechanism() {
+        return new Mechanism("SHA-256", channelBinding);
     }
 }

src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha512.java 🔗

@@ -17,8 +17,7 @@ public class HashedTokenSha512 extends HashedToken {
     }
 
     @Override
-    public String getMechanism() {
-        final String cbShortName = ChannelBinding.SHORT_NAMES.get(this.channelBinding);
-        return String.format("HT-SHA-512-%s", cbShortName);
+    public Mechanism getTokenMechanism() {
+        return new Mechanism("SHA-512", this.channelBinding);
     }
 }

src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java 🔗

@@ -166,9 +166,9 @@ public abstract class SaslMechanism {
 
     public static SaslMechanism ensureAvailable(
             final SaslMechanism mechanism, final SSLSockets.Version sslVersion) {
-        if (mechanism instanceof ScramPlusMechanism) {
-            final ChannelBinding cb = ((ScramPlusMechanism) mechanism).getChannelBinding();
-            if (ChannelBinding.ensureBest(cb, sslVersion)) {
+        if (mechanism instanceof ChannelBindingMechanism) {
+            final ChannelBinding cb = ((ChannelBindingMechanism) mechanism).getChannelBinding();
+            if (ChannelBinding.isAvailable(cb, sslVersion)) {
                 return mechanism;
             } else {
                 Log.d(

src/main/java/eu/siacs/conversations/crypto/sasl/ScramPlusMechanism.java 🔗

@@ -16,7 +16,7 @@ import javax.net.ssl.SSLSocket;
 
 import eu.siacs.conversations.entities.Account;
 
-public abstract class ScramPlusMechanism extends ScramMechanism {
+public abstract class ScramPlusMechanism extends ScramMechanism implements ChannelBindingMechanism {
 
     private static final String EXPORTER_LABEL = "EXPORTER-Channel-Binding";
 
@@ -103,6 +103,7 @@ public abstract class ScramPlusMechanism extends ScramMechanism {
         return messageDigest.digest();
     }
 
+    @Override
     public ChannelBinding getChannelBinding() {
         return this.channelBinding;
     }

src/main/java/eu/siacs/conversations/entities/Account.java 🔗

@@ -26,6 +26,9 @@ import eu.siacs.conversations.crypto.PgpDecryptionService;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 import eu.siacs.conversations.crypto.sasl.ChannelBinding;
+import eu.siacs.conversations.crypto.sasl.HashedToken;
+import eu.siacs.conversations.crypto.sasl.HashedTokenSha256;
+import eu.siacs.conversations.crypto.sasl.HashedTokenSha512;
 import eu.siacs.conversations.crypto.sasl.SaslMechanism;
 import eu.siacs.conversations.crypto.sasl.ScramPlusMechanism;
 import eu.siacs.conversations.services.AvatarService;
@@ -55,7 +58,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
     public static final String RESOURCE = "resource";
     public static final String PINNED_MECHANISM = "pinned_mechanism";
     public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding";
-
+    public static final String FAST_MECHANISM = "fast_mechanism";
+    public static final String FAST_TOKEN = "fast_token";
 
     public static final int OPTION_DISABLED = 1;
     public static final int OPTION_REGISTER = 2;
@@ -72,7 +76,6 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
     private static final String KEY_PINNED_MECHANISM = "pinned_mechanism";
     public static final String KEY_PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
 
-
     protected final JSONObject keys;
     private final Roster roster = new Roster(this);
     private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
@@ -101,16 +104,46 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
     private String presenceStatusMessage;
     private String pinnedMechanism;
     private String pinnedChannelBinding;
+    private String fastMechanism;
+    private String fastToken;
 
     public Account(final Jid jid, final String password) {
-        this(java.util.UUID.randomUUID().toString(), jid,
-                password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null, null, null);
-    }
-
-    private Account(final String uuid, final Jid jid,
-                    final String password, final int options, final String rosterVersion, final String keys,
-                    final String avatar, String displayName, String hostname, int port,
-                    final Presence.Status status, String statusMessage, final String pinnedMechanism, final String pinnedChannelBinding) {
+        this(
+                java.util.UUID.randomUUID().toString(),
+                jid,
+                password,
+                0,
+                null,
+                "",
+                null,
+                null,
+                null,
+                5222,
+                Presence.Status.ONLINE,
+                null,
+                null,
+                null,
+                null,
+                null);
+    }
+
+    private Account(
+            final String uuid,
+            final Jid jid,
+            final String password,
+            final int options,
+            final String rosterVersion,
+            final String keys,
+            final String avatar,
+            String displayName,
+            String hostname,
+            int port,
+            final Presence.Status status,
+            String statusMessage,
+            final String pinnedMechanism,
+            final String pinnedChannelBinding,
+            final String fastMechanism,
+            final String fastToken) {
         this.uuid = uuid;
         this.jid = jid;
         this.password = password;
@@ -131,21 +164,29 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
         this.presenceStatusMessage = statusMessage;
         this.pinnedMechanism = pinnedMechanism;
         this.pinnedChannelBinding = pinnedChannelBinding;
+        this.fastMechanism = fastMechanism;
+        this.fastToken = fastToken;
     }
 
     public static Account fromCursor(final Cursor cursor) {
         final Jid jid;
         try {
             final String resource = cursor.getString(cursor.getColumnIndexOrThrow(RESOURCE));
-            jid = Jid.of(
-                    cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)),
-                    cursor.getString(cursor.getColumnIndexOrThrow(SERVER)),
-                    resource == null || resource.trim().isEmpty() ? null : resource);
+            jid =
+                    Jid.of(
+                            cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)),
+                            cursor.getString(cursor.getColumnIndexOrThrow(SERVER)),
+                            resource == null || resource.trim().isEmpty() ? null : resource);
         } catch (final IllegalArgumentException e) {
-            Log.d(Config.LOGTAG, cursor.getString(cursor.getColumnIndexOrThrow(USERNAME)) + "@" + cursor.getString(cursor.getColumnIndexOrThrow(SERVER)));
+            Log.d(
+                    Config.LOGTAG,
+                    cursor.getString(cursor.getColumnIndexOrThrow(USERNAME))
+                            + "@"
+                            + cursor.getString(cursor.getColumnIndexOrThrow(SERVER)));
             throw new AssertionError(e);
         }
-        return new Account(cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
+        return new Account(
+                cursor.getString(cursor.getColumnIndexOrThrow(UUID)),
                 jid,
                 cursor.getString(cursor.getColumnIndexOrThrow(PASSWORD)),
                 cursor.getInt(cursor.getColumnIndexOrThrow(OPTIONS)),
@@ -155,10 +196,13 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
                 cursor.getString(cursor.getColumnIndexOrThrow(DISPLAY_NAME)),
                 cursor.getString(cursor.getColumnIndexOrThrow(HOSTNAME)),
                 cursor.getInt(cursor.getColumnIndexOrThrow(PORT)),
-                Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndexOrThrow(STATUS))),
+                Presence.Status.fromShowString(
+                        cursor.getString(cursor.getColumnIndexOrThrow(STATUS))),
                 cursor.getString(cursor.getColumnIndexOrThrow(STATUS_MESSAGE)),
                 cursor.getString(cursor.getColumnIndexOrThrow(PINNED_MECHANISM)),
-                cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING)));
+                cursor.getString(cursor.getColumnIndexOrThrow(PINNED_CHANNEL_BINDING)),
+                cursor.getString(cursor.getColumnIndexOrThrow(FAST_MECHANISM)),
+                cursor.getString(cursor.getColumnIndexOrThrow(FAST_TOKEN)));
     }
 
     public boolean httpUploadAvailable(long size) {
@@ -305,10 +349,18 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
     public void setPinnedMechanism(final SaslMechanism mechanism) {
         this.pinnedMechanism = mechanism.getMechanism();
         if (mechanism instanceof ScramPlusMechanism) {
-            this.pinnedChannelBinding = ((ScramPlusMechanism) mechanism).getChannelBinding().toString();
+            this.pinnedChannelBinding =
+                    ((ScramPlusMechanism) mechanism).getChannelBinding().toString();
+        } else {
+            this.pinnedChannelBinding = null;
         }
     }
 
+    public void setFastToken(final HashedToken.Mechanism mechanism, final String token) {
+        this.fastMechanism = mechanism.name();
+        this.fastToken = token;
+    }
+
     public void resetPinnedMechanism() {
         this.pinnedMechanism = null;
         this.pinnedChannelBinding = null;
@@ -328,12 +380,39 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
         }
     }
 
-    public SaslMechanism getPinnedMechanism() {
+    private SaslMechanism getPinnedMechanism() {
         final String mechanism = Strings.nullToEmpty(this.pinnedMechanism);
         final ChannelBinding channelBinding = ChannelBinding.get(this.pinnedChannelBinding);
         return new SaslMechanism.Factory(this).of(mechanism, channelBinding);
     }
 
+    private HashedToken getFastMechanism() {
+        final HashedToken.Mechanism fastMechanism = HashedToken.Mechanism.ofOrNull(this.fastMechanism);
+        final String token = this.fastToken;
+        if (fastMechanism == null || Strings.isNullOrEmpty(token)) {
+            return null;
+        }
+        if (fastMechanism.hashFunction.equals("SHA-256")) {
+            return new HashedTokenSha256(this, fastMechanism.channelBinding);
+        } else if (fastMechanism.hashFunction.equals("SHA-512")) {
+            return new HashedTokenSha512(this, fastMechanism.channelBinding);
+        } else {
+            return null;
+        }
+    }
+
+    public SaslMechanism getQuickStartMechanism() {
+        final HashedToken hashedTokenMechanism = getFastMechanism();
+        if (hashedTokenMechanism != null) {
+            return hashedTokenMechanism;
+        }
+        return getPinnedMechanism();
+    }
+
+    public String getFastToken() {
+        return this.fastToken;
+    }
+
     public State getTrueStatus() {
         return this.status;
     }
@@ -435,6 +514,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
         values.put(RESOURCE, jid.getResource());
         values.put(PINNED_MECHANISM, pinnedMechanism);
         values.put(PINNED_CHANNEL_BINDING, pinnedChannelBinding);
+        values.put(FAST_MECHANISM, this.fastMechanism);
+        values.put(FAST_TOKEN, this.fastToken);
         return values;
     }
 
@@ -480,7 +561,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
 
     public int activeDevicesWithRtpCapability() {
         int i = 0;
-        for(Presence presence : getSelfContact().getPresences().getPresences()) {
+        for (Presence presence : getSelfContact().getPresences().getPresences()) {
             if (RtpCapability.check(presence) != RtpCapability.Capability.NONE) {
                 i++;
             }
@@ -617,7 +698,9 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
 
     public String getShareableLink() {
         List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
-        String uri = "https://conversations.im/i/" + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString());
+        String uri =
+                "https://conversations.im/i/"
+                        + XmppUri.lameUrlEncode(this.getJid().asBareJid().toEscapedString());
         if (fingerprints.size() > 0) {
             return XmppUri.getFingerprintUri(uri, fingerprints, '&');
         } else {
@@ -630,10 +713,18 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
         if (axolotlService == null) {
             return fingerprints;
         }
-        fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, axolotlService.getOwnFingerprint().substring(2), axolotlService.getOwnDeviceId()));
+        fingerprints.add(
+                new XmppUri.Fingerprint(
+                        XmppUri.FingerprintType.OMEMO,
+                        axolotlService.getOwnFingerprint().substring(2),
+                        axolotlService.getOwnDeviceId()));
         for (XmppAxolotlSession session : axolotlService.findOwnSessions()) {
             if (session.getTrust().isVerified() && session.getTrust().isActive()) {
-                fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO, session.getFingerprint().substring(2).replaceAll("\\s", ""), session.getRemoteAddress().getDeviceId()));
+                fingerprints.add(
+                        new XmppUri.Fingerprint(
+                                XmppUri.FingerprintType.OMEMO,
+                                session.getFingerprint().substring(2).replaceAll("\\s", ""),
+                                session.getRemoteAddress().getDeviceId()));
             }
         }
         return fingerprints;
@@ -641,7 +732,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
 
     public boolean isBlocked(final ListItem contact) {
         final Jid jid = contact.getJid();
-        return jid != null && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
+        return jid != null
+                && (blocklist.contains(jid.asBareJid()) || blocklist.contains(jid.getDomain()));
     }
 
     public boolean isBlocked(final Jid jid) {
@@ -685,7 +777,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
         REGISTRATION_CONFLICT(true, false),
         REGISTRATION_NOT_SUPPORTED(true, false),
         REGISTRATION_PLEASE_WAIT(true, false),
-        REGISTRATION_INVALID_TOKEN(true,false),
+        REGISTRATION_INVALID_TOKEN(true, false),
         REGISTRATION_PASSWORD_TOO_WEAK(true, false),
         TLS_ERROR,
         TLS_ERROR_DOMAIN,

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -64,7 +64,7 @@ import eu.siacs.conversations.xmpp.mam.MamReference;
 public class DatabaseBackend extends SQLiteOpenHelper {
 
     private static final String DATABASE_NAME = "history";
-    private static final int DATABASE_VERSION = 50;
+    private static final int DATABASE_VERSION = 51;
 
     private static boolean requiresMessageIndexRebuild = false;
     private static DatabaseBackend instance = null;
@@ -232,6 +232,8 @@ public class DatabaseBackend extends SQLiteOpenHelper {
                 + Account.RESOURCE + " TEXT,"
                 + Account.PINNED_MECHANISM + " TEXT,"
                 + Account.PINNED_CHANNEL_BINDING + " TEXT,"
+                + Account.FAST_MECHANISM + " TEXT,"
+                + Account.FAST_TOKEN + " TEXT,"
                 + Account.PORT + " NUMBER DEFAULT 5222)");
         db.execSQL("create table " + Conversation.TABLENAME + " ("
                 + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME
@@ -594,7 +596,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
         if (oldVersion < 50 && newVersion >= 50) {
             db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_MECHANISM + " TEXT");
             db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PINNED_CHANNEL_BINDING + " TEXT");
-
+        }
+        if (oldVersion < 51 && newVersion >= 51) {
+            db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_MECHANISM + " TEXT");
+            db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.FAST_TOKEN + " TEXT");
         }
     }
 

src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java 🔗

@@ -757,7 +757,6 @@ public class XmppConnection implements Runnable {
                         Config.LOGTAG,
                         account.getJid().asBareJid()
                                 + ": jid changed during SASL 2.0. updating database");
-                mXmppConnectionService.databaseBackend.updateAccount(account);
             }
             final Element bound = success.findChild("bound", Namespace.BIND2);
             final Element resumed = success.findChild("resumed", "urn:xmpp:sm:3");
@@ -798,11 +797,21 @@ public class XmppConnection implements Runnable {
                 }
                 sendPostBindInitialization(waitForDisco, carbonsEnabled != null);
             }
-            //TODO figure out name either by the existence of hashTokenRequest or if scramMechanism is of instance HashedToken
-            if (this.hashTokenRequest != null && !Strings.isNullOrEmpty(token)) {
-                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+this.hashTokenRequest.name()+ " "+token);
+            final HashedToken.Mechanism tokenMechanism;
+            final SaslMechanism currentMechanism = this.saslMechanism;
+            if (currentMechanism instanceof HashedToken) {
+                tokenMechanism = ((HashedToken) currentMechanism).getTokenMechanism();
+            } else if (this.hashTokenRequest != null) {
+                tokenMechanism = this.hashTokenRequest;
+            } else {
+                tokenMechanism = null;
+            }
+            if (tokenMechanism != null && !Strings.isNullOrEmpty(token)) {
+                this.account.setFastToken(tokenMechanism,token);
+                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": storing hashed token "+tokenMechanism);
             }
         }
+        mXmppConnectionService.databaseBackend.updateAccount(account);
         this.quickStartInProgress = false;
         if (version == SaslMechanism.Version.SASL) {
             tagReader.reset();
@@ -826,6 +835,7 @@ public class XmppConnection implements Runnable {
         } catch (final IllegalArgumentException e) {
             throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
         }
+        Log.d(Config.LOGTAG,failure.toString());
         Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
         if (failure.hasChild("temporary-auth-failure")) {
             throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE);
@@ -1340,6 +1350,7 @@ public class XmppConnection implements Runnable {
         }
         final boolean quickStartAvailable;
         final String firstMessage = saslMechanism.getClientFirstMessage();
+        final boolean usingFast = saslMechanism instanceof HashedToken;
         final Element authenticate;
         if (version == SaslMechanism.Version.SASL) {
             authenticate = new Element("auth", Namespace.SASL);
@@ -1350,9 +1361,15 @@ public class XmppConnection implements Runnable {
         } else if (version == SaslMechanism.Version.SASL_2) {
             final Element inline = authElement.findChild("inline", Namespace.SASL_2);
             final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3");
-            final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST);
-            final Collection<String> fastMechanisms = SaslMechanism.mechanisms(fast);
-            final HashedToken.Mechanism hashTokenRequest = HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket));
+            final HashedToken.Mechanism hashTokenRequest;
+            if (usingFast) {
+                hashTokenRequest = null;
+            } else {
+                final Element fast = inline == null ? null : inline.findChild("fast", Namespace.FAST);
+                final Collection<String> fastMechanisms = SaslMechanism.mechanisms(fast);
+                hashTokenRequest =
+                        HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket));
+            }
             final Collection<String> bindFeatures = Bind2.features(inline);
             quickStartAvailable =
                     sm
@@ -1370,7 +1387,7 @@ public class XmppConnection implements Runnable {
                 }
             }
             this.hashTokenRequest = hashTokenRequest;
-            authenticate = generateAuthenticationRequest(firstMessage, hashTokenRequest, bindFeatures, sm);
+            authenticate = generateAuthenticationRequest(firstMessage, usingFast, hashTokenRequest, bindFeatures, sm);
         } else {
             throw new AssertionError("Missing implementation for " + version);
         }
@@ -1390,12 +1407,13 @@ public class XmppConnection implements Runnable {
         tagWriter.writeElement(authenticate);
     }
 
-    private Element generateAuthenticationRequest(final String firstMessage) {
-        return generateAuthenticationRequest(firstMessage, null, Bind2.QUICKSTART_FEATURES, true);
+    private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) {
+        return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
     }
 
     private Element generateAuthenticationRequest(
             final String firstMessage,
+            final boolean usingFast,
             final HashedToken.Mechanism hashedTokenRequest,
             final Collection<String> bind,
             final boolean inlineStreamManagement) {
@@ -1423,7 +1441,12 @@ public class XmppConnection implements Runnable {
             authenticate.addChild(resume);
         }
         if (hashedTokenRequest != null) {
-            authenticate.addChild("request-token", Namespace.FAST).setAttribute("mechanism", hashedTokenRequest.name());
+            authenticate
+                    .addChild("request-token", Namespace.FAST)
+                    .setAttribute("mechanism", hashedTokenRequest.name());
+        }
+        if (usingFast) {
+            authenticate.addChild("fast", Namespace.FAST);
         }
         return authenticate;
     }
@@ -2059,25 +2082,26 @@ public class XmppConnection implements Runnable {
 
     private boolean establishStream(final SSLSockets.Version sslVersion)
             throws IOException, InterruptedException {
-        final SaslMechanism pinnedMechanism =
-                SaslMechanism.ensureAvailable(account.getPinnedMechanism(), sslVersion);
+        final SaslMechanism quickStartMechanism =
+                SaslMechanism.ensureAvailable(account.getQuickStartMechanism(), sslVersion);
         final boolean secureConnection = sslVersion != SSLSockets.Version.NONE;
         if (secureConnection
                 && Config.QUICKSTART_ENABLED
-                && pinnedMechanism != null
+                && quickStartMechanism != null
                 && account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
             mXmppConnectionService.restoredFromDatabaseLatch.await();
-            this.saslMechanism = pinnedMechanism;
+            this.saslMechanism = quickStartMechanism;
+            final boolean usingFast = quickStartMechanism instanceof HashedToken;
             final Element authenticate =
-                    generateAuthenticationRequest(pinnedMechanism.getClientFirstMessage());
-            authenticate.setAttribute("mechanism", pinnedMechanism.getMechanism());
+                    generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(), usingFast);
+            authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism());
             sendStartStream(true, false);
             tagWriter.writeElement(authenticate);
             Log.d(
                     Config.LOGTAG,
                     account.getJid().toString()
                             + ": quick start with "
-                            + pinnedMechanism.getMechanism());
+                            + quickStartMechanism.getMechanism());
             return true;
         } else {
             sendStartStream(secureConnection, true);