diff --git a/conversations.doap b/conversations.doap index 653cb9fc7ba55acda35529294365055fb3b7c153..fa2427d14dc28ab819841180ee32dfacfd371a0f 100644 --- a/conversations.doap +++ b/conversations.doap @@ -490,6 +490,13 @@ 0.1.0 + + + + complete + 0.3.1 + + diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DowngradeProtection.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DowngradeProtection.java new file mode 100644 index 0000000000000000000000000000000000000000..6daaa8809398e6f4aa2c7d311ed894fc0ae05299 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/DowngradeProtection.java @@ -0,0 +1,98 @@ +package eu.siacs.conversations.crypto.sasl; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Ordering; +import java.util.Collection; + +public class DowngradeProtection { + + private static final char SEPARATOR = ','; + private static final char SEPARATOR_MECHANISM_AND_BINDING = '|'; + + public final ImmutableList mechanisms; + public final ImmutableList channelBindings; + + public DowngradeProtection( + final Collection mechanisms, final Collection channelBindings) { + this.mechanisms = Ordering.natural().immutableSortedCopy(mechanisms); + this.channelBindings = Ordering.natural().immutableSortedCopy(channelBindings); + } + + public DowngradeProtection(final Collection mechanisms) { + this.mechanisms = Ordering.natural().immutableSortedCopy(mechanisms); + this.channelBindings = null; + } + + public String asDString() { + ensureSaslMechanismFormat(this.mechanisms); + ensureNoSeparators(this.mechanisms); + if (this.channelBindings != null) { + ensureNoSeparators(this.channelBindings); + ensureBindingFormat(this.channelBindings); + final var builder = new StringBuilder(); + Joiner.on(SEPARATOR).appendTo(builder, mechanisms); + builder.append(SEPARATOR_MECHANISM_AND_BINDING); + Joiner.on(SEPARATOR).appendTo(builder, channelBindings); + return builder.toString(); + } else { + return Joiner.on(SEPARATOR).join(mechanisms); + } + } + + private static void ensureNoSeparators(final Iterable list) { + for (final String item : list) { + if (item.indexOf(SEPARATOR) >= 0 + || item.indexOf(SEPARATOR_MECHANISM_AND_BINDING) >= 0) { + throw new SecurityException("illegal chars found in list"); + } + } + } + + private static void ensureSaslMechanismFormat(final Iterable names) { + for (final String name : names) { + ensureSaslMechanismFormat(name); + } + } + + private static void ensureSaslMechanismFormat(final String name) { + if (Strings.isNullOrEmpty(name)) { + throw new SecurityException("Empty sasl mechanism names are not permitted"); + } + // https://www.rfc-editor.org/rfc/rfc4422.html#section-3.1 + if (name.length() <= 20 + && CharMatcher.inRange('A', 'Z') + .or(CharMatcher.inRange('0', '9')) + .or(CharMatcher.is('-')) + .or(CharMatcher.is('_')) + .matchesAllOf(name) + && !Character.isDigit(name.charAt(0))) { + return; + } + throw new SecurityException("Encountered illegal sasl name"); + } + + private static void ensureBindingFormat(final Iterable names) { + for (final String name : names) { + ensureBindingFormat(name); + } + } + + private static void ensureBindingFormat(final String name) { + if (Strings.isNullOrEmpty(name)) { + throw new SecurityException("Empty binding names are not permitted"); + } + // https://www.rfc-editor.org/rfc/rfc5056.html#section-7d + if (CharMatcher.inRange('A', 'Z') + .or(CharMatcher.inRange('a', 'z')) + .or(CharMatcher.inRange('0', '9')) + .or(CharMatcher.is('.')) + .or(CharMatcher.is('-')) + .matchesAllOf(name)) { + return; + } + throw new SecurityException("Encountered illegal binding name"); + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 935f953bd1cc2b5f68645a36b91a09735a6bb361..97ae1600ecfe8a95d4d34b7fc8253174110f3b84 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.crypto.sasl; import com.google.common.base.CaseFormat; import com.google.common.base.Joiner; import com.google.common.base.Objects; +import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.cache.Cache; @@ -20,7 +21,7 @@ import java.util.concurrent.ExecutionException; import javax.crypto.SecretKey; import javax.net.ssl.SSLSocket; -abstract class ScramMechanism extends SaslMechanism { +public abstract class ScramMechanism extends SaslMechanism { public static final SecretKey EMPTY_KEY = new SecretKey() { @@ -50,6 +51,7 @@ abstract class ScramMechanism extends SaslMechanism { protected State state = State.INITIAL; private final String clientFirstMessageBare; private byte[] serverSignature = null; + private DowngradeProtection downgradeProtection = null; ScramMechanism(final Account account, final ChannelBinding channelBinding) { super(account); @@ -76,6 +78,12 @@ abstract class ScramMechanism extends SaslMechanism { this.clientNonce); } + public void setDowngradeProtection(final DowngradeProtection downgradeProtection) { + Preconditions.checkState( + this.state == State.INITIAL, "setting downgrade protection in invalid state"); + this.downgradeProtection = downgradeProtection; + } + protected abstract HashFunction getHMac(final byte[] key); protected abstract HashFunction getDigest(); @@ -128,9 +136,8 @@ abstract class ScramMechanism extends SaslMechanism { @Override public String getClientFirstMessage(final SSLSocket sslSocket) { - if (this.state != State.INITIAL) { - throw new IllegalArgumentException("Calling getClientFirstMessage from invalid state"); - } + Preconditions.checkState( + this.state == State.INITIAL, "Calling getClientFirstMessage from invalid state"); this.state = State.AUTH_TEXT_SENT; final byte[] message = (gs2Header + clientFirstMessageBare).getBytes(); return BaseEncoding.base64().encode(message); @@ -198,6 +205,19 @@ abstract class ScramMechanism extends SaslMechanism { throw new AuthenticationException("Invalid salt in server first message"); } + if (d != null && this.downgradeProtection != null) { + final String asSeenInFeatures; + try { + asSeenInFeatures = downgradeProtection.asDString(); + } catch (final SecurityException e) { + throw new AuthenticationException(e); + } + final var hashed = BaseEncoding.base64().encode(digest(asSeenInFeatures.getBytes())); + if (!hashed.equals(d)) { + throw new AuthenticationException("Mismatch in SSDP"); + } + } + final byte[] channelBindingData = getChannelBindingData(socket); final int gs2Len = this.gs2Header.getBytes().length; diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index c2f799036970208fd9860d3f88757761acabeb34..6ce05e3784a07f1ca336b22bb7ff776b59187cae 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -29,8 +29,10 @@ 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.ChannelBindingMechanism; +import eu.siacs.conversations.crypto.sasl.DowngradeProtection; import eu.siacs.conversations.crypto.sasl.HashedToken; import eu.siacs.conversations.crypto.sasl.SaslMechanism; +import eu.siacs.conversations.crypto.sasl.ScramMechanism; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.ServiceDiscoveryResult; @@ -1558,6 +1560,16 @@ public class XmppConnection implements Runnable { final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket)); this.validate(saslMechanism, mechanisms); + final DowngradeProtection downgradeProtection; + if (cbExtension != null) { + downgradeProtection = + new DowngradeProtection(mechanisms, cbExtension.getChannelBindingTypes()); + } else { + downgradeProtection = new DowngradeProtection(mechanisms); + } + if (saslMechanism instanceof ScramMechanism scramMechanism) { + scramMechanism.setDowngradeProtection(downgradeProtection); + } final boolean quickStartAvailable; final String firstMessage = saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)); diff --git a/src/main/java/im/conversations/android/xmpp/model/cb/SaslChannelBinding.java b/src/main/java/im/conversations/android/xmpp/model/cb/SaslChannelBinding.java index 545655814e77f3704cf465aa3bd382c810b3a2e2..c01ce1b9858114b64c96cbad72758d8967bf3d4f 100644 --- a/src/main/java/im/conversations/android/xmpp/model/cb/SaslChannelBinding.java +++ b/src/main/java/im/conversations/android/xmpp/model/cb/SaslChannelBinding.java @@ -1,5 +1,7 @@ package im.conversations.android.xmpp.model.cb; +import com.google.common.base.Predicates; +import com.google.common.collect.Collections2; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.StreamFeature; import java.util.Collection; @@ -14,4 +16,10 @@ public class SaslChannelBinding extends StreamFeature { public Collection getChannelBindings() { return this.getExtensions(ChannelBinding.class); } + + public Collection getChannelBindingTypes() { + return Collections2.filter( + Collections2.transform(getChannelBindings(), ChannelBinding::getType), + Predicates.notNull()); + } }