pipeline sasl2 directly after stream start

This commit is contained in:
Daniel Gultsch 2022-09-25 14:13:04 +02:00
parent 126e8ef08c
commit 32f9a58d9a
6 changed files with 121 additions and 42 deletions

View file

@ -57,16 +57,14 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding"; public static final String PINNED_CHANNEL_BINDING = "pinned_channel_binding";
public static final int OPTION_USETLS = 0;
public static final int OPTION_DISABLED = 1; public static final int OPTION_DISABLED = 1;
public static final int OPTION_REGISTER = 2; public static final int OPTION_REGISTER = 2;
public static final int OPTION_USECOMPRESSION = 3;
public static final int OPTION_MAGIC_CREATE = 4; public static final int OPTION_MAGIC_CREATE = 4;
public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5; public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5;
public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6; public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6;
public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7; public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7;
public static final int OPTION_UNVERIFIED = 8;
public static final int OPTION_FIXED_USERNAME = 9; public static final int OPTION_FIXED_USERNAME = 9;
public static final int OPTION_QUICKSTART_AVAILABLE = 10;
private static final String KEY_PGP_SIGNATURE = "pgp_signature"; private static final String KEY_PGP_SIGNATURE = "pgp_signature";
private static final String KEY_PGP_ID = "pgp_id"; private static final String KEY_PGP_ID = "pgp_id";

View file

@ -286,8 +286,6 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat
mAccount = new Account(jid.asBareJid(), password); mAccount = new Account(jid.asBareJid(), password);
mAccount.setPort(numericPort); mAccount.setPort(numericPort);
mAccount.setHostname(hostname); mAccount.setHostname(hostname);
mAccount.setOption(Account.OPTION_USETLS, true);
mAccount.setOption(Account.OPTION_USECOMPRESSION, true);
mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
xmppConnectionService.createAccount(mAccount); xmppConnectionService.createAccount(mAccount);
} }

View file

@ -1,6 +1,7 @@
package eu.siacs.conversations.xml; package eu.siacs.conversations.xml;
public final class Namespace { public final class Namespace {
public static final String STREAMS = "http://etherx.jabber.org/streams";
public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items"; public static final String DISCO_ITEMS = "http://jabber.org/protocol/disco#items";
public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info"; public static final String DISCO_INFO = "http://jabber.org/protocol/disco#info";
public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2"; public static final String EXTERNAL_SERVICE_DISCOVERY = "urn:xmpp:extdisco:2";

View file

@ -58,15 +58,20 @@ public class TagWriter {
throw new IOException("output stream was null"); throw new IOException("output stream was null");
} }
outputStream.write("<?xml version='1.0'?>"); outputStream.write("<?xml version='1.0'?>");
outputStream.flush();
} }
public synchronized void writeTag(Tag tag) throws IOException { public void writeTag(final Tag tag) throws IOException {
writeTag(tag, true);
}
public synchronized void writeTag(final Tag tag, final boolean flush) throws IOException {
if (outputStream == null) { if (outputStream == null) {
throw new IOException("output stream was null"); throw new IOException("output stream was null");
} }
outputStream.write(tag.toString()); outputStream.write(tag.toString());
outputStream.flush(); if (flush) {
outputStream.flush();
}
} }
public synchronized void writeElement(Element element) throws IOException { public synchronized void writeElement(Element element) throws IOException {

View file

@ -89,6 +89,7 @@ import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.Tag;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
import eu.siacs.conversations.xml.XmlReader; import eu.siacs.conversations.xml.XmlReader;
import eu.siacs.conversations.xmpp.bind.Bind2;
import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
@ -155,6 +156,7 @@ public class XmppConnection implements Runnable {
private TagWriter tagWriter = new TagWriter(); private TagWriter tagWriter = new TagWriter();
private boolean shouldAuthenticate = true; private boolean shouldAuthenticate = true;
private boolean inSmacksSession = false; private boolean inSmacksSession = false;
private boolean quickStartInProgress = false;
private boolean isBound = false; private boolean isBound = false;
private Element streamFeatures; private Element streamFeatures;
private String streamId = null; private String streamId = null;
@ -270,11 +272,11 @@ public class XmppConnection implements Runnable {
} }
Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting"); Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": connecting");
features.encryptionEnabled = false; features.encryptionEnabled = false;
inSmacksSession = false; this.inSmacksSession = false;
isBound = false; this.quickStartInProgress = false;
this.isBound = false;
this.attempt++; this.attempt++;
this.verifiedHostname = this.verifiedHostname = null; // will be set if user entered hostname is being used or hostname was verified
null; // will be set if user entered hostname is being used or hostname was verified
// with dnssec // with dnssec
try { try {
Socket localSocket; Socket localSocket;
@ -310,14 +312,14 @@ public class XmppConnection implements Runnable {
try { try {
startXmpp(localSocket); startXmpp(localSocket);
} catch (InterruptedException e) { } catch (final InterruptedException e) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
account.getJid().asBareJid() account.getJid().asBareJid()
+ ": thread was interrupted before beginning stream"); + ": thread was interrupted before beginning stream");
return; return;
} catch (Exception e) { } catch (final Exception e) {
throw new IOException(e.getMessage()); throw new IOException("Could not start stream", e);
} }
} else { } else {
final String domain = account.getServer(); final String domain = account.getServer();
@ -477,7 +479,7 @@ public class XmppConnection implements Runnable {
* *
* @return true if server returns with valid xmpp, false otherwise * @return true if server returns with valid xmpp, false otherwise
*/ */
private boolean startXmpp(Socket socket) throws Exception { private boolean startXmpp(final Socket socket) throws Exception {
if (Thread.currentThread().isInterrupted()) { if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException(); throw new InterruptedException();
} }
@ -490,15 +492,22 @@ public class XmppConnection implements Runnable {
tagWriter.setOutputStream(socket.getOutputStream()); tagWriter.setOutputStream(socket.getOutputStream());
tagReader.setInputStream(socket.getInputStream()); tagReader.setInputStream(socket.getInputStream());
tagWriter.beginDocument(); tagWriter.beginDocument();
sendStartStream(); final boolean quickStart;
if (socket instanceof SSLSocket) {
SSLSocketHelper.log(account, (SSLSocket) socket);
quickStart = establishStream(true);
} else {
quickStart = establishStream(false);
}
final Tag tag = tagReader.readTag(); final Tag tag = tagReader.readTag();
if (Thread.currentThread().isInterrupted()) { if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException(); throw new InterruptedException();
} }
if (socket instanceof SSLSocket) { final boolean success = tag != null && tag.isStart("stream", Namespace.STREAMS);
SSLSocketHelper.log(account, (SSLSocket) socket); if (success && quickStart) {
this.quickStartInProgress = true;
} }
return tag != null && tag.isStart("stream"); return success;
} }
private SSLSocketFactory getSSLSocketFactory() private SSLSocketFactory getSSLSocketFactory()
@ -761,11 +770,12 @@ public class XmppConnection implements Runnable {
sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null); sendPostBindInitialization(streamManagementEnabled != null, carbonsEnabled != null);
} }
} }
this.quickStartInProgress = false;
if (version == SaslMechanism.Version.SASL) { if (version == SaslMechanism.Version.SASL) {
tagReader.reset(); tagReader.reset();
sendStartStream(); sendStartStream(true);
final Tag tag = tagReader.readTag(); final Tag tag = tagReader.readTag();
if (tag != null && tag.isStart("stream")) { if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
processStream(); processStream();
return true; return true;
} else { } else {
@ -1119,11 +1129,14 @@ public class XmppConnection implements Runnable {
final SSLSocket sslSocket = upgradeSocketToTls(socket); final SSLSocket sslSocket = upgradeSocketToTls(socket);
tagReader.setInputStream(sslSocket.getInputStream()); tagReader.setInputStream(sslSocket.getInputStream());
tagWriter.setOutputStream(sslSocket.getOutputStream()); tagWriter.setOutputStream(sslSocket.getOutputStream());
sendStartStream();
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established"); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": TLS connection established");
final boolean quickStart = establishStream(true);
if (quickStart) {
this.quickStartInProgress = true;
}
features.encryptionEnabled = true; features.encryptionEnabled = true;
final Tag tag = tagReader.readTag(); final Tag tag = tagReader.readTag();
if (tag != null && tag.isStart("stream")) { if (tag != null && tag.isStart("stream", Namespace.STREAMS)) {
SSLSocketHelper.log(account, sslSocket); SSLSocketHelper.log(account, sslSocket);
processStream(); processStream();
} else { } else {
@ -1170,7 +1183,13 @@ public class XmppConnection implements Runnable {
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);
if (this.streamFeatures.hasChild("starttls", Namespace.TLS) if (this.quickStartInProgress) {
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
+ ": quick start in progress. ignoring features: "
+ XmlHelper.printElementNames(this.streamFeatures));
} else if (this.streamFeatures.hasChild("starttls", Namespace.TLS)
&& !features.encryptionEnabled) { && !features.encryptionEnabled) {
sendStartTLS(); sendStartTLS();
} else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE) } else if (this.streamFeatures.hasChild("register", Namespace.REGISTER_STREAM_FEATURE)
@ -1238,6 +1257,7 @@ public class XmppConnection implements Runnable {
} else { } else {
authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2); authElement = this.streamFeatures.findChild("authentication", Namespace.SASL_2);
} }
//TODO externalize
final Collection<String> mechanisms = final Collection<String> mechanisms =
Collections2.transform( Collections2.transform(
Collections2.filter( Collections2.filter(
@ -1261,6 +1281,8 @@ public class XmppConnection implements Runnable {
final SaslMechanism.Factory factory = new SaslMechanism.Factory(account); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
this.saslMechanism = factory.of(mechanisms, channelBindings); this.saslMechanism = factory.of(mechanisms, channelBindings);
//TODO externalize checks
if (saslMechanism == null) { if (saslMechanism == null) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
@ -1282,6 +1304,7 @@ 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);
} }
final boolean quickStartAvailable;
final String firstMessage = saslMechanism.getClientFirstMessage(); final String firstMessage = saslMechanism.getClientFirstMessage();
final Element authenticate; final Element authenticate;
if (version == SaslMechanism.Version.SASL) { if (version == SaslMechanism.Version.SASL) {
@ -1289,15 +1312,24 @@ public class XmppConnection implements Runnable {
if (!Strings.isNullOrEmpty(firstMessage)) { if (!Strings.isNullOrEmpty(firstMessage)) {
authenticate.setContent(firstMessage); authenticate.setContent(firstMessage);
} }
quickStartAvailable = false;
} else if (version == SaslMechanism.Version.SASL_2) { } else if (version == SaslMechanism.Version.SASL_2) {
final Element inline = authElement.findChild("inline", Namespace.SASL_2); final Element inline = authElement.findChild("inline", Namespace.SASL_2);
final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3"); final boolean sm = inline != null && inline.hasChild("sm", "urn:xmpp:sm:3");
final Collection<String> bindFeatures = bindFeatures(inline); final Collection<String> bindFeatures = Bind2.features(inline);
quickStartAvailable =
sm
&& bindFeatures != null
&& bindFeatures.containsAll(Bind2.QUICKSTART_FEATURES);
authenticate = generateAuthenticationRequest(firstMessage, bindFeatures, sm); authenticate = generateAuthenticationRequest(firstMessage, bindFeatures, sm);
} else { } else {
throw new AssertionError("Missing implementation for " + version); throw new AssertionError("Missing implementation for " + version);
} }
if (account.setOption(Account.OPTION_QUICKSTART_AVAILABLE, quickStartAvailable)) {
mXmppConnectionService.updateAccount(account);
}
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
account.getJid().toString() account.getJid().toString()
@ -1309,19 +1341,8 @@ public class XmppConnection implements Runnable {
tagWriter.writeElement(authenticate); tagWriter.writeElement(authenticate);
} }
private static Collection<String> bindFeatures(final Element inline) { private Element generateAuthenticationRequest(final String firstMessage) {
final Element inlineBind2 = return generateAuthenticationRequest(firstMessage, Bind2.QUICKSTART_FEATURES, true);
inline != null ? inline.findChild("bind", Namespace.BIND2) : null;
final Element inlineBind2Inline =
inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null;
if (inlineBind2 == null) {
return null;
}
if (inlineBind2Inline == null) {
return Collections.emptyList();
}
return Collections2.transform(
inlineBind2Inline.getChildren(), c -> c == null ? null : c.getAttribute("var"));
} }
private Element generateAuthenticationRequest( private Element generateAuthenticationRequest(
@ -1988,14 +2009,37 @@ public class XmppConnection implements Runnable {
} }
} }
private void sendStartStream() throws IOException { private boolean establishStream(final boolean secureConnection) throws IOException {
final SaslMechanism saslMechanism = account.getPinnedMechanism();
if (secureConnection
&& saslMechanism != null
&& account.isOptionSet(Account.OPTION_QUICKSTART_AVAILABLE)) {
this.saslMechanism = saslMechanism;
final Element authenticate =
generateAuthenticationRequest(saslMechanism.getClientFirstMessage());
authenticate.setAttribute("mechanism", saslMechanism.getMechanism());
sendStartStream(false);
tagWriter.writeElement(authenticate);
Log.d(
Config.LOGTAG,
account.getJid().toString()
+ ": quick start with "
+ saslMechanism.getMechanism());
return true;
} else {
sendStartStream(true);
return false;
}
}
private void sendStartStream(final boolean flush) throws IOException {
final Tag stream = Tag.start("stream:stream"); final Tag stream = Tag.start("stream:stream");
stream.setAttribute("to", account.getServer()); stream.setAttribute("to", account.getServer());
stream.setAttribute("version", "1.0"); stream.setAttribute("version", "1.0");
stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE); stream.setAttribute("xml:lang", LocalizedContent.STREAM_LANGUAGE);
stream.setAttribute("xmlns", "jabber:client"); stream.setAttribute("xmlns", "jabber:client");
stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams"); stream.setAttribute("xmlns:stream", Namespace.STREAMS);
tagWriter.writeTag(stream); tagWriter.writeTag(stream, flush);
} }
private String createNewResource() { private String createNewResource() {

View file

@ -0,0 +1,33 @@
package eu.siacs.conversations.xmpp.bind;
import com.google.common.collect.Collections2;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace;
public class Bind2 {
public static final Collection<String> QUICKSTART_FEATURES = Arrays.asList(
Namespace.CARBONS,
Namespace.STREAM_MANAGEMENT
);
public static Collection<String> features(final Element inline) {
final Element inlineBind2 =
inline != null ? inline.findChild("bind", Namespace.BIND2) : null;
final Element inlineBind2Inline =
inlineBind2 != null ? inlineBind2.findChild("inline", Namespace.BIND2) : null;
if (inlineBind2 == null) {
return null;
}
if (inlineBind2Inline == null) {
return Collections.emptyList();
}
return Collections2.transform(
inlineBind2Inline.getChildren(), c -> c == null ? null : c.getAttribute("var"));
}
}