parse hash token names

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/Config.java                        |   2 
src/main/java/eu/siacs/conversations/crypto/sasl/ChannelBinding.java    |  33 
src/main/java/eu/siacs/conversations/crypto/sasl/HashedToken.java       | 114 
src/main/java/eu/siacs/conversations/crypto/sasl/HashedTokenSha256.java |  24 
src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java           |   3 
5 files changed, 172 insertions(+), 4 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 = true;
+    public static final boolean QUICKSTART_ENABLED = false;
 
     //Notification settings
     public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false;

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

@@ -6,7 +6,9 @@ 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 java.util.Arrays;
 import java.util.Collection;
@@ -23,6 +25,16 @@ public enum ChannelBinding {
     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
@@ -85,7 +97,24 @@ public enum ChannelBinding {
         }
     }
 
-    public static boolean ensureBest(final ChannelBinding channelBinding, final SSLSockets.Version sslVersion) {
-        return ChannelBinding.best(Collections.singleton(channelBinding), sslVersion) == channelBinding;
+    public static boolean ensureBest(
+            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);
+        }
     }
 }

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

@@ -0,0 +1,114 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.hash.HashFunction;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import javax.net.ssl.SSLSocket;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.SSLSockets;
+
+public abstract class HashedToken extends SaslMechanism {
+
+    private static List<String> HASH_FUNCTIONS = Arrays.asList("SHA-512", "SHA-256");
+
+    protected final ChannelBinding channelBinding;
+
+    protected HashedToken(final Account account, final ChannelBinding channelBinding) {
+        super(account);
+        this.channelBinding = channelBinding;
+    }
+
+    @Override
+    public int getPriority() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getClientFirstMessage() {
+        return null; // HMAC(token, "Initiator" || cb-data)
+    }
+
+    @Override
+    public String getResponse(final String challenge, final SSLSocket socket)
+            throws AuthenticationException {
+        // todo verify that challenge matches HMAC(token, "Responder" || cb-data)
+        return null;
+    }
+
+    protected abstract HashFunction getHashFunction(final byte[] key);
+
+    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("HT")) {
+                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 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();
+        }
+    }
+}

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

@@ -0,0 +1,24 @@
+package eu.siacs.conversations.crypto.sasl;
+
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+import eu.siacs.conversations.entities.Account;
+
+public class HashedTokenSha256 extends HashedToken {
+
+    public HashedTokenSha256(final Account account, final ChannelBinding channelBinding) {
+        super(account, channelBinding);
+    }
+
+    @Override
+    protected HashFunction getHashFunction(final byte[] key) {
+        return Hashing.hmacSha256(key);
+    }
+
+    @Override
+    public String getMechanism() {
+        final String cbShortName = ChannelBinding.SHORT_NAMES.get(this.channelBinding);
+        return String.format("HT-SHA-256-%s", cbShortName);
+    }
+}

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

@@ -63,6 +63,7 @@ import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.XmppDomainVerifier;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.sasl.ChannelBinding;
+import eu.siacs.conversations.crypto.sasl.HashedToken;
 import eu.siacs.conversations.crypto.sasl.SaslMechanism;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Message;
@@ -1344,7 +1345,7 @@ public class XmppConnection implements Runnable {
             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);
-            Log.d(Config.LOGTAG,"fast mechanisms: "+fastMechanisms);
+            Log.d(Config.LOGTAG,"fast mechanism: "+ HashedToken.Mechanism.best(fastMechanisms, SSLSockets.version(this.socket)));
             final Collection<String> bindFeatures = Bind2.features(inline);
             quickStartAvailable =
                     sm