support logging in via SASL 2

This commit is contained in:
Daniel Gultsch 2022-08-29 17:09:52 +02:00
parent a717917b3d
commit 5fc8ff899a
2 changed files with 124 additions and 59 deletions

View file

@ -1,8 +1,12 @@
package eu.siacs.conversations.crypto.sasl; package eu.siacs.conversations.crypto.sasl;
import com.google.common.base.Strings;
import java.security.SecureRandom; import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
public abstract class SaslMechanism { public abstract class SaslMechanism {
@ -68,6 +72,17 @@ public abstract class SaslMechanism {
} }
public enum Version { public enum Version {
SASL, SASL_2 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");
}
}
} }
} }

View file

@ -469,16 +469,36 @@ public class XmppConnection implements Runnable {
} else if (nextTag.isStart("proceed")) { } else if (nextTag.isStart("proceed")) {
switchOverToTls(); switchOverToTls();
} else if (nextTag.isStart("success")) { } else if (nextTag.isStart("success")) {
final String challenge = tagReader.readElement(nextTag).getContent(); final Element success = tagReader.readElement(nextTag);
final SaslMechanism.Version version;
try {
version = SaslMechanism.Version.of(success);
} catch (final IllegalArgumentException e) {
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
final String challenge;
if (version == SaslMechanism.Version.SASL) {
challenge = success.getContent();
} else if (version == SaslMechanism.Version.SASL_2) {
challenge = success.findChildContent("additional-data");
} else {
throw new AssertionError("Missing implementation for " + version);
}
try { try {
saslMechanism.getResponse(challenge); saslMechanism.getResponse(challenge);
} catch (final SaslMechanism.AuthenticationException e) { } catch (final SaslMechanism.AuthenticationException e) {
Log.e(Config.LOGTAG, String.valueOf(e)); Log.e(Config.LOGTAG, String.valueOf(e));
throw new StateChangingException(Account.State.UNAUTHORIZED); throw new StateChangingException(Account.State.UNAUTHORIZED);
} }
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": logged in"); Log.d(
account.setKey(Account.PINNED_MECHANISM_KEY, Config.LOGTAG,
String.valueOf(saslMechanism.getPriority())); account.getJid().asBareJid().toString()
+ ": logged in (using "
+ version
+ ")");
account.setKey(
Account.PINNED_MECHANISM_KEY, String.valueOf(saslMechanism.getPriority()));
if (version == SaslMechanism.Version.SASL) {
tagReader.reset(); tagReader.reset();
sendStartStream(); sendStartStream();
final Tag tag = tagReader.readTag(); final Tag tag = tagReader.readTag();
@ -488,9 +508,19 @@ public class XmppConnection implements Runnable {
throw new StateChangingException(Account.State.STREAM_OPENING_ERROR); throw new StateChangingException(Account.State.STREAM_OPENING_ERROR);
} }
break; break;
}
} else if (nextTag.isStart("failure")) { } else if (nextTag.isStart("failure")) {
final Element failure = tagReader.readElement(nextTag); final Element failure = tagReader.readElement(nextTag);
if (Namespace.SASL.equals(failure.getNamespace())) { if (Namespace.TLS.equals(failure.getNamespace())) {
throw new StateChangingException(Account.State.TLS_ERROR);
}
final SaslMechanism.Version version;
try {
version = SaslMechanism.Version.of(failure);
} catch (final IllegalArgumentException e) {
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
if (failure.hasChild("temporary-auth-failure")) { if (failure.hasChild("temporary-auth-failure")) {
throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE); throw new StateChangingException(Account.State.TEMPORARY_AUTH_FAILURE);
} else if (failure.hasChild("account-disabled")) { } else if (failure.hasChild("account-disabled")) {
@ -513,19 +543,28 @@ public class XmppConnection implements Runnable {
} }
} }
throw new StateChangingException(Account.State.UNAUTHORIZED); throw new StateChangingException(Account.State.UNAUTHORIZED);
} else if (Namespace.TLS.equals(failure.getNamespace())) { } else if (nextTag.isStart("challenge")) {
throw new StateChangingException(Account.State.TLS_ERROR); final Element challenge = tagReader.readElement(nextTag);
} else { final SaslMechanism.Version version;
try {
version = SaslMechanism.Version.of(challenge);
} catch (final IllegalArgumentException e) {
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER); throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
} }
} else if (nextTag.isStart("challenge")) { final Element response;
final String challenge = tagReader.readElement(nextTag).getContent(); if (version == SaslMechanism.Version.SASL) {
final Element response = new Element("response", Namespace.SASL); response = new Element("response", Namespace.SASL);
} else if (version == SaslMechanism.Version.SASL_2) {
response = new Element("response", Namespace.SASL_2);
} else {
throw new AssertionError("Missing implementation for " + version);
}
try { try {
response.setContent(saslMechanism.getResponse(challenge)); response.setContent(saslMechanism.getResponse(challenge.getContent()));
} catch (final SaslMechanism.AuthenticationException e) { } catch (final SaslMechanism.AuthenticationException e) {
// TODO: Send auth abort tag. // TODO: Send auth abort tag.
Log.e(Config.LOGTAG, e.toString()); Log.e(Config.LOGTAG, e.toString());
throw new StateChangingException(Account.State.UNAUTHORIZED);
} }
tagWriter.writeElement(response); tagWriter.writeElement(response);
} else if (nextTag.isStart("enabled")) { } else if (nextTag.isStart("enabled")) {
@ -848,7 +887,6 @@ public class XmppConnection implements Runnable {
private void processStreamFeatures(final Tag currentTag) throws IOException { private void processStreamFeatures(final Tag currentTag) throws IOException {
this.streamFeatures = tagReader.readElement(currentTag); this.streamFeatures = tagReader.readElement(currentTag);
Log.d(Config.LOGTAG, this.streamFeatures.toString());
final boolean isSecure = final boolean isSecure =
features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion();
final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
@ -907,7 +945,6 @@ public class XmppConnection implements Runnable {
private void authenticate(final SaslMechanism.Version version) throws IOException { private void authenticate(final SaslMechanism.Version version) throws IOException {
final List<String> mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms")); final List<String> mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms"));
final Element auth = new Element("auth", Namespace.SASL);
if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) {
saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains(ScramSha512.MECHANISM)) { } else if (mechanisms.contains(ScramSha512.MECHANISM)) {
@ -923,7 +960,10 @@ public class XmppConnection implements Runnable {
} else if (mechanisms.contains(Anonymous.MECHANISM)) { } else if (mechanisms.contains(Anonymous.MECHANISM)) {
saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG()); saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG());
} }
if (saslMechanism != null) { if (saslMechanism == null) {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find supported SASL mechanism in " + mechanisms);
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
}
final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1); final int pinnedMechanism = account.getKeyAsInt(Account.PINNED_MECHANISM_KEY, -1);
if (pinnedMechanism > saslMechanism.getPriority()) { if (pinnedMechanism > saslMechanism.getPriority()) {
Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() + Log.e(Config.LOGTAG, "Auth failed. Authentication mechanism " + saslMechanism.getMechanism() +
@ -932,16 +972,26 @@ public class XmppConnection implements Runnable {
"). Possible downgrade attack?"); "). Possible downgrade attack?");
throw new StateChangingException(Account.State.DOWNGRADE_ATTACK); throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
} }
Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with " + saslMechanism.getMechanism()); final String firstMessage = saslMechanism.getClientFirstMessage();
auth.setAttribute("mechanism", saslMechanism.getMechanism()); final Element authenticate;
if (!saslMechanism.getClientFirstMessage().isEmpty()) { if (version == SaslMechanism.Version.SASL) {
auth.setContent(saslMechanism.getClientFirstMessage()); authenticate = new Element("auth", Namespace.SASL);
if (!Strings.isNullOrEmpty(firstMessage)) {
authenticate.setContent(firstMessage);
} }
tagWriter.writeElement(auth); } else if (version == SaslMechanism.Version.SASL_2) {
authenticate = new Element("authenticate", Namespace.SASL_2);
if (!Strings.isNullOrEmpty(firstMessage)) {
authenticate.addChild("initial-response").setContent(firstMessage);
}
// TODO place to add extensions
} else { } else {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to find supported SASL mechanism in " + mechanisms); throw new AssertionError("Missing implementation for " + version);
throw new StateChangingException(Account.State.INCOMPATIBLE_SERVER);
} }
Log.d(Config.LOGTAG, account.getJid().toString() + ": Authenticating with "+version+ "/" + saslMechanism.getMechanism());
authenticate.setAttribute("mechanism", saslMechanism.getMechanism());
tagWriter.writeElement(authenticate);
} }
private List<String> extractMechanisms(final Element stream) { private List<String> extractMechanisms(final Element stream) {