support logging in via SASL 2
This commit is contained in:
parent
a717917b3d
commit
5fc8ff899a
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,14 +508,24 @@ 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")) {
|
||||||
final String text = failure.findChildContent("text");
|
final String text = failure.findChildContent("text");
|
||||||
if ( Strings.isNullOrEmpty(text)) {
|
if (Strings.isNullOrEmpty(text)) {
|
||||||
throw new StateChangingException(Account.State.UNAUTHORIZED);
|
throw new StateChangingException(Account.State.UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text);
|
final Matcher matcher = Patterns.AUTOLINK_WEB_URL.matcher(text);
|
||||||
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue