add support for HashedToken channel binding
This commit is contained in:
parent
24badda4c9
commit
e2b9f0e77a
|
@ -1,5 +1,7 @@
|
||||||
package eu.siacs.conversations.crypto.sasl;
|
package eu.siacs.conversations.crypto.sasl;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
|
||||||
public class Anonymous extends SaslMechanism {
|
public class Anonymous extends SaslMechanism {
|
||||||
|
@ -21,7 +23,7 @@ public class Anonymous extends SaslMechanism {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientFirstMessage() {
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,100 @@
|
||||||
package eu.siacs.conversations.crypto.sasl;
|
package eu.siacs.conversations.crypto.sasl;
|
||||||
|
|
||||||
|
import org.bouncycastle.jcajce.provider.digest.SHA256;
|
||||||
|
import org.conscrypt.Conscrypt;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
public interface ChannelBindingMechanism {
|
public interface ChannelBindingMechanism {
|
||||||
|
|
||||||
|
String EXPORTER_LABEL = "EXPORTER-Channel-Binding";
|
||||||
|
|
||||||
ChannelBinding getChannelBinding();
|
ChannelBinding getChannelBinding();
|
||||||
|
|
||||||
|
static byte[] getChannelBindingData(final SSLSocket sslSocket, final ChannelBinding channelBinding)
|
||||||
|
throws SaslMechanism.AuthenticationException {
|
||||||
|
if (sslSocket == null) {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Channel binding attempt on non secure socket");
|
||||||
|
}
|
||||||
|
if (channelBinding == ChannelBinding.TLS_EXPORTER) {
|
||||||
|
final byte[] keyingMaterial;
|
||||||
|
try {
|
||||||
|
keyingMaterial =
|
||||||
|
Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32);
|
||||||
|
} catch (final SSLException e) {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Could not export keying material");
|
||||||
|
}
|
||||||
|
if (keyingMaterial == null) {
|
||||||
|
throw new SaslMechanism.AuthenticationException(
|
||||||
|
"Could not export keying material. Socket not ready");
|
||||||
|
}
|
||||||
|
return keyingMaterial;
|
||||||
|
} else if (channelBinding == ChannelBinding.TLS_UNIQUE) {
|
||||||
|
final byte[] unique = Conscrypt.getTlsUnique(sslSocket);
|
||||||
|
if (unique == null) {
|
||||||
|
throw new SaslMechanism.AuthenticationException(
|
||||||
|
"Could not retrieve tls unique. Socket not ready");
|
||||||
|
}
|
||||||
|
return unique;
|
||||||
|
} else if (channelBinding == ChannelBinding.TLS_SERVER_END_POINT) {
|
||||||
|
return getServerEndPointChannelBinding(sslSocket.getSession());
|
||||||
|
} else {
|
||||||
|
throw new SaslMechanism.AuthenticationException(
|
||||||
|
String.format("%s is not a valid channel binding", channelBinding));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] getServerEndPointChannelBinding(final SSLSession session)
|
||||||
|
throws SaslMechanism.AuthenticationException {
|
||||||
|
final Certificate[] certificates;
|
||||||
|
try {
|
||||||
|
certificates = session.getPeerCertificates();
|
||||||
|
} catch (final SSLPeerUnverifiedException e) {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Could not verify peer certificates");
|
||||||
|
}
|
||||||
|
if (certificates == null || certificates.length == 0) {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Could not retrieve peer certificate");
|
||||||
|
}
|
||||||
|
final X509Certificate certificate;
|
||||||
|
if (certificates[0] instanceof X509Certificate) {
|
||||||
|
certificate = (X509Certificate) certificates[0];
|
||||||
|
} else {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Certificate was not X509");
|
||||||
|
}
|
||||||
|
final String algorithm = certificate.getSigAlgName();
|
||||||
|
final int withIndex = algorithm.indexOf("with");
|
||||||
|
if (withIndex <= 0) {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Unable to parse SigAlgName");
|
||||||
|
}
|
||||||
|
final String hashAlgorithm = algorithm.substring(0, withIndex);
|
||||||
|
final MessageDigest messageDigest;
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc5929#section-4.1
|
||||||
|
if ("MD5".equalsIgnoreCase(hashAlgorithm) || "SHA1".equalsIgnoreCase(hashAlgorithm)) {
|
||||||
|
messageDigest = new SHA256.Digest();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
messageDigest = MessageDigest.getInstance(hashAlgorithm);
|
||||||
|
} catch (final NoSuchAlgorithmException e) {
|
||||||
|
throw new SaslMechanism.AuthenticationException(
|
||||||
|
"Could not instantiate message digest for " + hashAlgorithm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final byte[] encodedCertificate;
|
||||||
|
try {
|
||||||
|
encodedCertificate = certificate.getEncoded();
|
||||||
|
} catch (final CertificateEncodingException e) {
|
||||||
|
throw new SaslMechanism.AuthenticationException("Could not encode certificate");
|
||||||
|
}
|
||||||
|
messageDigest.update(encodedCertificate);
|
||||||
|
return messageDigest.digest();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package eu.siacs.conversations.crypto.sasl;
|
||||||
|
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
|
||||||
public class External extends SaslMechanism {
|
public class External extends SaslMechanism {
|
||||||
|
@ -23,7 +25,7 @@ public class External extends SaslMechanism {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientFirstMessage() {
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
return Base64.encodeToString(
|
return Base64.encodeToString(
|
||||||
account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP);
|
account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.siacs.conversations.crypto.sasl;
|
package eu.siacs.conversations.crypto.sasl;
|
||||||
|
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects;
|
import com.google.common.base.MoreObjects;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
@ -18,6 +19,7 @@ import java.util.List;
|
||||||
|
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
import eu.siacs.conversations.utils.SSLSockets;
|
import eu.siacs.conversations.utils.SSLSockets;
|
||||||
|
|
||||||
|
@ -42,10 +44,10 @@ public abstract class HashedToken extends SaslMechanism implements ChannelBindin
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientFirstMessage() {
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
final String token = Strings.nullToEmpty(this.account.getFastToken());
|
final String token = Strings.nullToEmpty(this.account.getFastToken());
|
||||||
final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8));
|
final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8));
|
||||||
final byte[] cbData = new byte[0];
|
final byte[] cbData = getChannelBindingData(sslSocket);
|
||||||
final byte[] initiatorHashedToken =
|
final byte[] initiatorHashedToken =
|
||||||
hashing.hashBytes(Bytes.concat(INITIATOR, cbData)).asBytes();
|
hashing.hashBytes(Bytes.concat(INITIATOR, cbData)).asBytes();
|
||||||
final byte[] firstMessage =
|
final byte[] firstMessage =
|
||||||
|
@ -56,6 +58,23 @@ public abstract class HashedToken extends SaslMechanism implements ChannelBindin
|
||||||
return Base64.encodeToString(firstMessage, Base64.NO_WRAP);
|
return Base64.encodeToString(firstMessage, Base64.NO_WRAP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] getChannelBindingData(final SSLSocket sslSocket) {
|
||||||
|
if (this.channelBinding == ChannelBinding.NONE) {
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding);
|
||||||
|
} catch (final AuthenticationException e) {
|
||||||
|
Log.e(
|
||||||
|
Config.LOGTAG,
|
||||||
|
account.getJid().asBareJid()
|
||||||
|
+ ": unable to retrieve channel binding data for "
|
||||||
|
+ getMechanism(),
|
||||||
|
e);
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getResponse(final String challenge, final SSLSocket socket)
|
public String getResponse(final String challenge, final SSLSocket socket)
|
||||||
throws AuthenticationException {
|
throws AuthenticationException {
|
||||||
|
@ -67,7 +86,7 @@ public abstract class HashedToken extends SaslMechanism implements ChannelBindin
|
||||||
}
|
}
|
||||||
final String token = Strings.nullToEmpty(this.account.getFastToken());
|
final String token = Strings.nullToEmpty(this.account.getFastToken());
|
||||||
final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8));
|
final HashFunction hashing = getHashFunction(token.getBytes(StandardCharsets.UTF_8));
|
||||||
final byte[] cbData = new byte[0];
|
final byte[] cbData = getChannelBindingData(socket);
|
||||||
final byte[] expectedResponderMessage =
|
final byte[] expectedResponderMessage =
|
||||||
hashing.hashBytes(Bytes.concat(RESPONDER, cbData)).asBytes();
|
hashing.hashBytes(Bytes.concat(RESPONDER, cbData)).asBytes();
|
||||||
if (Arrays.equals(responderMessage, expectedResponderMessage)) {
|
if (Arrays.equals(responderMessage, expectedResponderMessage)) {
|
||||||
|
|
|
@ -4,6 +4,8 @@ import android.util.Base64;
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
|
||||||
public class Plain extends SaslMechanism {
|
public class Plain extends SaslMechanism {
|
||||||
|
@ -30,7 +32,7 @@ public class Plain extends SaslMechanism {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientFirstMessage() {
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
return getMessage(account.getUsername(), account.getPassword());
|
return getMessage(account.getUsername(), account.getPassword());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ public abstract class SaslMechanism {
|
||||||
|
|
||||||
public abstract String getMechanism();
|
public abstract String getMechanism();
|
||||||
|
|
||||||
public String getClientFirstMessage() {
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +154,12 @@ public abstract class SaslMechanism {
|
||||||
public SaslMechanism of(
|
public SaslMechanism of(
|
||||||
final Collection<String> mechanisms,
|
final Collection<String> mechanisms,
|
||||||
final Collection<ChannelBinding> bindings,
|
final Collection<ChannelBinding> bindings,
|
||||||
|
final Version version,
|
||||||
final SSLSockets.Version sslVersion) {
|
final SSLSockets.Version sslVersion) {
|
||||||
|
final HashedToken fastMechanism = account.getFastMechanism();
|
||||||
|
if (version == Version.SASL_2 && fastMechanism != null) {
|
||||||
|
return fastMechanism;
|
||||||
|
}
|
||||||
final ChannelBinding channelBinding = ChannelBinding.best(bindings, sslVersion);
|
final ChannelBinding channelBinding = ChannelBinding.best(bindings, sslVersion);
|
||||||
return of(mechanisms, channelBinding);
|
return of(mechanisms, channelBinding);
|
||||||
}
|
}
|
||||||
|
@ -180,4 +185,12 @@ public abstract class SaslMechanism {
|
||||||
return mechanism;
|
return mechanism;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean hashedToken(final SaslMechanism saslMechanism) {
|
||||||
|
return saslMechanism instanceof HashedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean pin(final SaslMechanism saslMechanism) {
|
||||||
|
return !hashedToken(saslMechanism);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,7 +112,7 @@ abstract class ScramMechanism extends SaslMechanism {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientFirstMessage() {
|
public String getClientFirstMessage(final SSLSocket sslSocket) {
|
||||||
if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
|
if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
|
||||||
clientFirstMessageBare =
|
clientFirstMessageBare =
|
||||||
"n="
|
"n="
|
||||||
|
|
|
@ -1,25 +1,11 @@
|
||||||
package eu.siacs.conversations.crypto.sasl;
|
package eu.siacs.conversations.crypto.sasl;
|
||||||
|
|
||||||
import org.bouncycastle.jcajce.provider.digest.SHA256;
|
|
||||||
import org.conscrypt.Conscrypt;
|
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.cert.Certificate;
|
|
||||||
import java.security.cert.CertificateEncodingException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
|
|
||||||
import javax.net.ssl.SSLException;
|
|
||||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
|
||||||
import javax.net.ssl.SSLSession;
|
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
|
|
||||||
import eu.siacs.conversations.entities.Account;
|
import eu.siacs.conversations.entities.Account;
|
||||||
|
|
||||||
public abstract class ScramPlusMechanism extends ScramMechanism implements ChannelBindingMechanism {
|
public abstract class ScramPlusMechanism extends ScramMechanism implements ChannelBindingMechanism {
|
||||||
|
|
||||||
private static final String EXPORTER_LABEL = "EXPORTER-Channel-Binding";
|
|
||||||
|
|
||||||
ScramPlusMechanism(Account account, ChannelBinding channelBinding) {
|
ScramPlusMechanism(Account account, ChannelBinding channelBinding) {
|
||||||
super(account, channelBinding);
|
super(account, channelBinding);
|
||||||
}
|
}
|
||||||
|
@ -27,80 +13,7 @@ public abstract class ScramPlusMechanism extends ScramMechanism implements Chann
|
||||||
@Override
|
@Override
|
||||||
protected byte[] getChannelBindingData(final SSLSocket sslSocket)
|
protected byte[] getChannelBindingData(final SSLSocket sslSocket)
|
||||||
throws AuthenticationException {
|
throws AuthenticationException {
|
||||||
if (sslSocket == null) {
|
return ChannelBindingMechanism.getChannelBindingData(sslSocket, this.channelBinding);
|
||||||
throw new AuthenticationException("Channel binding attempt on non secure socket");
|
|
||||||
}
|
|
||||||
if (this.channelBinding == ChannelBinding.TLS_EXPORTER) {
|
|
||||||
final byte[] keyingMaterial;
|
|
||||||
try {
|
|
||||||
keyingMaterial =
|
|
||||||
Conscrypt.exportKeyingMaterial(sslSocket, EXPORTER_LABEL, new byte[0], 32);
|
|
||||||
} catch (final SSLException e) {
|
|
||||||
throw new AuthenticationException("Could not export keying material");
|
|
||||||
}
|
|
||||||
if (keyingMaterial == null) {
|
|
||||||
throw new AuthenticationException(
|
|
||||||
"Could not export keying material. Socket not ready");
|
|
||||||
}
|
|
||||||
return keyingMaterial;
|
|
||||||
} else if (this.channelBinding == ChannelBinding.TLS_UNIQUE) {
|
|
||||||
final byte[] unique = Conscrypt.getTlsUnique(sslSocket);
|
|
||||||
if (unique == null) {
|
|
||||||
throw new AuthenticationException(
|
|
||||||
"Could not retrieve tls unique. Socket not ready");
|
|
||||||
}
|
|
||||||
return unique;
|
|
||||||
} else if (this.channelBinding == ChannelBinding.TLS_SERVER_END_POINT) {
|
|
||||||
return getServerEndPointChannelBinding(sslSocket.getSession());
|
|
||||||
} else {
|
|
||||||
throw new AuthenticationException(
|
|
||||||
String.format("%s is not a valid channel binding", channelBinding));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] getServerEndPointChannelBinding(final SSLSession session)
|
|
||||||
throws AuthenticationException {
|
|
||||||
final Certificate[] certificates;
|
|
||||||
try {
|
|
||||||
certificates = session.getPeerCertificates();
|
|
||||||
} catch (final SSLPeerUnverifiedException e) {
|
|
||||||
throw new AuthenticationException("Could not verify peer certificates");
|
|
||||||
}
|
|
||||||
if (certificates == null || certificates.length == 0) {
|
|
||||||
throw new AuthenticationException("Could not retrieve peer certificate");
|
|
||||||
}
|
|
||||||
final X509Certificate certificate;
|
|
||||||
if (certificates[0] instanceof X509Certificate) {
|
|
||||||
certificate = (X509Certificate) certificates[0];
|
|
||||||
} else {
|
|
||||||
throw new AuthenticationException("Certificate was not X509");
|
|
||||||
}
|
|
||||||
final String algorithm = certificate.getSigAlgName();
|
|
||||||
final int withIndex = algorithm.indexOf("with");
|
|
||||||
if (withIndex <= 0) {
|
|
||||||
throw new AuthenticationException("Unable to parse SigAlgName");
|
|
||||||
}
|
|
||||||
final String hashAlgorithm = algorithm.substring(0, withIndex);
|
|
||||||
final MessageDigest messageDigest;
|
|
||||||
// https://www.rfc-editor.org/rfc/rfc5929#section-4.1
|
|
||||||
if ("MD5".equalsIgnoreCase(hashAlgorithm) || "SHA1".equalsIgnoreCase(hashAlgorithm)) {
|
|
||||||
messageDigest = new SHA256.Digest();
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
messageDigest = MessageDigest.getInstance(hashAlgorithm);
|
|
||||||
} catch (final NoSuchAlgorithmException e) {
|
|
||||||
throw new AuthenticationException(
|
|
||||||
"Could not instantiate message digest for " + hashAlgorithm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
final byte[] encodedCertificate;
|
|
||||||
try {
|
|
||||||
encodedCertificate = certificate.getEncoded();
|
|
||||||
} catch (final CertificateEncodingException e) {
|
|
||||||
throw new AuthenticationException("Could not encode certificate");
|
|
||||||
}
|
|
||||||
messageDigest.update(encodedCertificate);
|
|
||||||
return messageDigest.digest();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -26,6 +26,7 @@ import eu.siacs.conversations.crypto.PgpDecryptionService;
|
||||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
|
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
|
||||||
import eu.siacs.conversations.crypto.sasl.ChannelBinding;
|
import eu.siacs.conversations.crypto.sasl.ChannelBinding;
|
||||||
|
import eu.siacs.conversations.crypto.sasl.ChannelBindingMechanism;
|
||||||
import eu.siacs.conversations.crypto.sasl.HashedToken;
|
import eu.siacs.conversations.crypto.sasl.HashedToken;
|
||||||
import eu.siacs.conversations.crypto.sasl.HashedTokenSha256;
|
import eu.siacs.conversations.crypto.sasl.HashedTokenSha256;
|
||||||
import eu.siacs.conversations.crypto.sasl.HashedTokenSha512;
|
import eu.siacs.conversations.crypto.sasl.HashedTokenSha512;
|
||||||
|
@ -348,9 +349,9 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
|
|
||||||
public void setPinnedMechanism(final SaslMechanism mechanism) {
|
public void setPinnedMechanism(final SaslMechanism mechanism) {
|
||||||
this.pinnedMechanism = mechanism.getMechanism();
|
this.pinnedMechanism = mechanism.getMechanism();
|
||||||
if (mechanism instanceof ScramPlusMechanism) {
|
if (mechanism instanceof ChannelBindingMechanism) {
|
||||||
this.pinnedChannelBinding =
|
this.pinnedChannelBinding =
|
||||||
((ScramPlusMechanism) mechanism).getChannelBinding().toString();
|
((ChannelBindingMechanism) mechanism).getChannelBinding().toString();
|
||||||
} else {
|
} else {
|
||||||
this.pinnedChannelBinding = null;
|
this.pinnedChannelBinding = null;
|
||||||
}
|
}
|
||||||
|
@ -386,7 +387,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
||||||
return new SaslMechanism.Factory(this).of(mechanism, channelBinding);
|
return new SaslMechanism.Factory(this).of(mechanism, channelBinding);
|
||||||
}
|
}
|
||||||
|
|
||||||
private HashedToken getFastMechanism() {
|
public HashedToken getFastMechanism() {
|
||||||
final HashedToken.Mechanism fastMechanism = HashedToken.Mechanism.ofOrNull(this.fastMechanism);
|
final HashedToken.Mechanism fastMechanism = HashedToken.Mechanism.ofOrNull(this.fastMechanism);
|
||||||
final String token = this.fastToken;
|
final String token = this.fastToken;
|
||||||
if (fastMechanism == null || Strings.isNullOrEmpty(token)) {
|
if (fastMechanism == null || Strings.isNullOrEmpty(token)) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import android.util.Pair;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
|
|
||||||
|
@ -704,7 +705,9 @@ public class XmppConnection implements Runnable {
|
||||||
Log.d(
|
Log.d(
|
||||||
Config.LOGTAG,
|
Config.LOGTAG,
|
||||||
account.getJid().asBareJid().toString() + ": logged in (using " + version + ")");
|
account.getJid().asBareJid().toString() + ": logged in (using " + version + ")");
|
||||||
account.setPinnedMechanism(saslMechanism);
|
if (SaslMechanism.pin(this.saslMechanism)) {
|
||||||
|
account.setPinnedMechanism(this.saslMechanism);
|
||||||
|
}
|
||||||
if (version == SaslMechanism.Version.SASL_2) {
|
if (version == SaslMechanism.Version.SASL_2) {
|
||||||
final Tag tag = tagReader.readTag();
|
final Tag tag = tagReader.readTag();
|
||||||
if (tag != null && tag.isStart("features", Namespace.STREAMS)) {
|
if (tag != null && tag.isStart("features", Namespace.STREAMS)) {
|
||||||
|
@ -837,6 +840,7 @@ public class XmppConnection implements Runnable {
|
||||||
}
|
}
|
||||||
Log.d(Config.LOGTAG,failure.toString());
|
Log.d(Config.LOGTAG,failure.toString());
|
||||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
|
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": login failure " + version);
|
||||||
|
//TODO check if we are doing FAST; reset token
|
||||||
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")) {
|
||||||
|
@ -1242,6 +1246,7 @@ public class XmppConnection implements Runnable {
|
||||||
account.getJid().asBareJid()
|
account.getJid().asBareJid()
|
||||||
+ ": quick start in progress. ignoring features: "
|
+ ": quick start in progress. ignoring features: "
|
||||||
+ XmlHelper.printElementNames(this.streamFeatures));
|
+ XmlHelper.printElementNames(this.streamFeatures));
|
||||||
|
//TODO check if 'fast' is available but we are doing something else
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server lost support for SASL 2. quick start not possible");
|
Log.d(Config.LOGTAG,account.getJid().asBareJid()+": server lost support for SASL 2. quick start not possible");
|
||||||
|
@ -1320,37 +1325,12 @@ public class XmppConnection implements Runnable {
|
||||||
final Element cbElement =
|
final Element cbElement =
|
||||||
this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
|
this.streamFeatures.findChild("sasl-channel-binding", Namespace.CHANNEL_BINDING);
|
||||||
final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbElement);
|
final Collection<ChannelBinding> channelBindings = ChannelBinding.of(cbElement);
|
||||||
Log.d(Config.LOGTAG,"mechanisms: "+mechanisms);
|
|
||||||
Log.d(Config.LOGTAG, "channel bindings: " + channelBindings);
|
|
||||||
final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
|
final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
|
||||||
this.saslMechanism = factory.of(mechanisms, channelBindings, SSLSockets.version(this.socket));
|
final SaslMechanism saslMechanism = factory.of(mechanisms, channelBindings, version, SSLSockets.version(this.socket));
|
||||||
|
this.saslMechanism = validate(saslMechanism, mechanisms);
|
||||||
//TODO externalize checks
|
|
||||||
|
|
||||||
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.getPinnedMechanismPriority();
|
|
||||||
if (pinnedMechanism > saslMechanism.getPriority()) {
|
|
||||||
Log.e(
|
|
||||||
Config.LOGTAG,
|
|
||||||
"Auth failed. Authentication mechanism "
|
|
||||||
+ saslMechanism.getMechanism()
|
|
||||||
+ " has lower priority ("
|
|
||||||
+ saslMechanism.getPriority()
|
|
||||||
+ ") than pinned priority ("
|
|
||||||
+ pinnedMechanism
|
|
||||||
+ "). Possible downgrade attack?");
|
|
||||||
throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
|
|
||||||
}
|
|
||||||
final boolean quickStartAvailable;
|
final boolean quickStartAvailable;
|
||||||
final String firstMessage = saslMechanism.getClientFirstMessage();
|
final String firstMessage = this.saslMechanism.getClientFirstMessage(sslSocketOrNull(this.socket));
|
||||||
final boolean usingFast = saslMechanism instanceof HashedToken;
|
final boolean usingFast = SaslMechanism.hashedToken(this.saslMechanism);
|
||||||
final Element authenticate;
|
final Element authenticate;
|
||||||
if (version == SaslMechanism.Version.SASL) {
|
if (version == SaslMechanism.Version.SASL) {
|
||||||
authenticate = new Element("auth", Namespace.SASL);
|
authenticate = new Element("auth", Namespace.SASL);
|
||||||
|
@ -1402,11 +1382,40 @@ public class XmppConnection implements Runnable {
|
||||||
+ ": Authenticating with "
|
+ ": Authenticating with "
|
||||||
+ version
|
+ version
|
||||||
+ "/"
|
+ "/"
|
||||||
+ saslMechanism.getMechanism());
|
+ this.saslMechanism.getMechanism());
|
||||||
authenticate.setAttribute("mechanism", saslMechanism.getMechanism());
|
authenticate.setAttribute("mechanism", this.saslMechanism.getMechanism());
|
||||||
tagWriter.writeElement(authenticate);
|
tagWriter.writeElement(authenticate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private SaslMechanism validate(final @Nullable SaslMechanism saslMechanism, Collection<String> mechanisms) throws StateChangingException {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (SaslMechanism.hashedToken(saslMechanism)) {
|
||||||
|
return saslMechanism;
|
||||||
|
}
|
||||||
|
final int pinnedMechanism = account.getPinnedMechanismPriority();
|
||||||
|
if (pinnedMechanism > saslMechanism.getPriority()) {
|
||||||
|
Log.e(
|
||||||
|
Config.LOGTAG,
|
||||||
|
"Auth failed. Authentication mechanism "
|
||||||
|
+ saslMechanism.getMechanism()
|
||||||
|
+ " has lower priority ("
|
||||||
|
+ saslMechanism.getPriority()
|
||||||
|
+ ") than pinned priority ("
|
||||||
|
+ pinnedMechanism
|
||||||
|
+ "). Possible downgrade attack?");
|
||||||
|
throw new StateChangingException(Account.State.DOWNGRADE_ATTACK);
|
||||||
|
}
|
||||||
|
return saslMechanism;
|
||||||
|
}
|
||||||
|
|
||||||
private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) {
|
private Element generateAuthenticationRequest(final String firstMessage, final boolean usingFast) {
|
||||||
return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
|
return generateAuthenticationRequest(firstMessage, usingFast, null, Bind2.QUICKSTART_FEATURES, true);
|
||||||
}
|
}
|
||||||
|
@ -2093,7 +2102,7 @@ public class XmppConnection implements Runnable {
|
||||||
this.saslMechanism = quickStartMechanism;
|
this.saslMechanism = quickStartMechanism;
|
||||||
final boolean usingFast = quickStartMechanism instanceof HashedToken;
|
final boolean usingFast = quickStartMechanism instanceof HashedToken;
|
||||||
final Element authenticate =
|
final Element authenticate =
|
||||||
generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(), usingFast);
|
generateAuthenticationRequest(quickStartMechanism.getClientFirstMessage(sslSocketOrNull(this.socket)), usingFast);
|
||||||
authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism());
|
authenticate.setAttribute("mechanism", quickStartMechanism.getMechanism());
|
||||||
sendStartStream(true, false);
|
sendStartStream(true, false);
|
||||||
tagWriter.writeElement(authenticate);
|
tagWriter.writeElement(authenticate);
|
||||||
|
|
Loading…
Reference in a new issue