refactor SASL choice into factory; remove unused TagWriter

This commit is contained in:
Daniel Gultsch 2022-09-06 09:25:09 +02:00
parent 511dfa13c4
commit a210568a9c
19 changed files with 288 additions and 231 deletions

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.crypto.axolotl; package eu.siacs.conversations.crypto.axolotl;
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.os.Bundle; import android.os.Bundle;
import android.security.KeyChain; import android.security.KeyChain;
import android.util.Log; import android.util.Log;
@ -499,7 +501,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias()); PrivateKey x509PrivateKey = KeyChain.getPrivateKey(mXmppConnectionService, account.getPrivateKeyAlias());
X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias()); X509Certificate[] chain = KeyChain.getCertificateChain(mXmppConnectionService, account.getPrivateKeyAlias());
Signature verifier = Signature.getInstance("sha256WithRSA"); Signature verifier = Signature.getInstance("sha256WithRSA");
verifier.initSign(x509PrivateKey, mXmppConnectionService.getRNG()); verifier.initSign(x509PrivateKey, SECURE_RANDOM);
verifier.update(axolotlPublicKey.serialize()); verifier.update(axolotlPublicKey.serialize());
byte[] signature = verifier.sign(); byte[] signature = verifier.sign();
IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId()); IqPacket packet = mXmppConnectionService.getIqGenerator().publishVerification(signature, chain, getOwnDeviceId());

View file

@ -1,16 +1,13 @@
package eu.siacs.conversations.crypto.sasl; package eu.siacs.conversations.crypto.sasl;
import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter;
public class Anonymous extends SaslMechanism { public class Anonymous extends SaslMechanism {
public static final String MECHANISM = "ANONYMOUS"; public static final String MECHANISM = "ANONYMOUS";
public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) { public Anonymous(final Account account) {
super(tagWriter, account, rng); super(account);
} }
@Override @Override

View file

@ -5,18 +5,17 @@ import android.util.Base64;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xml.TagWriter;
public class DigestMd5 extends SaslMechanism { public class DigestMd5 extends SaslMechanism {
public static final String MECHANISM = "DIGEST-MD5"; public static final String MECHANISM = "DIGEST-MD5";
private State state = State.INITIAL;
public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) { public DigestMd5(final Account account) {
super(tagWriter, account, rng); super(account);
} }
@Override @Override
@ -29,8 +28,6 @@ public class DigestMd5 extends SaslMechanism {
return MECHANISM; return MECHANISM;
} }
private State state = State.INITIAL;
@Override @Override
public String getResponse(final String challenge) throws AuthenticationException { public String getResponse(final String challenge) throws AuthenticationException {
switch (state) { switch (state) {
@ -38,7 +35,8 @@ public class DigestMd5 extends SaslMechanism {
state = State.RESPONSE_SENT; state = State.RESPONSE_SENT;
final String encodedResponse; final String encodedResponse;
try { try {
final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); final Tokenizer tokenizer =
new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
String nonce = ""; String nonce = "";
for (final String token : tokenizer) { for (final String token : tokenizer) {
final String[] parts = token.split("=", 2); final String[] parts = token.split("=", 2);
@ -50,29 +48,49 @@ public class DigestMd5 extends SaslMechanism {
} }
final String digestUri = "xmpp/" + account.getServer(); final String digestUri = "xmpp/" + account.getServer();
final String nonceCount = "00000001"; final String nonceCount = "00000001";
final String x = account.getUsername() + ":" + account.getServer() + ":" final String x =
+ account.getPassword(); account.getUsername()
+ ":"
+ account.getServer()
+ ":"
+ account.getPassword();
final MessageDigest md = MessageDigest.getInstance("MD5"); final MessageDigest md = MessageDigest.getInstance("MD5");
final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); final byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
final String cNonce = CryptoHelper.random(100, rng); final String cNonce = CryptoHelper.random(100);
final byte[] a1 = CryptoHelper.concatenateByteArrays(y, final byte[] a1 =
(":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset())); CryptoHelper.concatenateByteArrays(
y,
(":" + nonce + ":" + cNonce)
.getBytes(Charset.defaultCharset()));
final String a2 = "AUTHENTICATE:" + digestUri; final String a2 = "AUTHENTICATE:" + digestUri;
final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); final String ha1 = CryptoHelper.bytesToHex(md.digest(a1));
final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset final String ha2 =
.defaultCharset()))); CryptoHelper.bytesToHex(
final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce md.digest(a2.getBytes(Charset.defaultCharset())));
+ ":auth:" + ha2; final String kd =
final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + ":auth:" + ha2;
.defaultCharset()))); final String response =
final String saslString = "username=\"" + account.getUsername() CryptoHelper.bytesToHex(
+ "\",realm=\"" + account.getServer() + "\",nonce=\"" md.digest(kd.getBytes(Charset.defaultCharset())));
+ nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount final String saslString =
+ ",qop=auth,digest-uri=\"" + digestUri + "\",response=" "username=\""
+ response + ",charset=utf-8"; + account.getUsername()
encodedResponse = Base64.encodeToString( + "\",realm=\""
saslString.getBytes(Charset.defaultCharset()), + account.getServer()
Base64.NO_WRAP); + "\",nonce=\""
+ nonce
+ "\",cnonce=\""
+ cNonce
+ "\",nc="
+ nonceCount
+ ",qop=auth,digest-uri=\""
+ digestUri
+ "\",response="
+ response
+ ",charset=utf-8";
encodedResponse =
Base64.encodeToString(
saslString.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
} catch (final NoSuchAlgorithmException e) { } catch (final NoSuchAlgorithmException e) {
throw new AuthenticationException(e); throw new AuthenticationException(e);
} }
@ -83,7 +101,7 @@ public class DigestMd5 extends SaslMechanism {
break; break;
case VALID_SERVER_RESPONSE: case VALID_SERVER_RESPONSE:
if (challenge == null) { if (challenge == null) {
return null; //everything is fine return null; // everything is fine
} }
default: default:
throw new InvalidStateException(state); throw new InvalidStateException(state);

View file

@ -2,17 +2,14 @@ package eu.siacs.conversations.crypto.sasl;
import android.util.Base64; import android.util.Base64;
import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter;
public class External extends SaslMechanism { public class External extends SaslMechanism {
public static final String MECHANISM = "EXTERNAL"; public static final String MECHANISM = "EXTERNAL";
public External(TagWriter tagWriter, Account account, SecureRandom rng) { public External(final Account account) {
super(tagWriter, account, rng); super(account);
} }
@Override @Override
@ -27,6 +24,7 @@ public class External extends SaslMechanism {
@Override @Override
public String getClientFirstMessage() { public String getClientFirstMessage() {
return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP); return Base64.encodeToString(
account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP);
} }
} }

View file

@ -5,14 +5,18 @@ import android.util.Base64;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter;
public class Plain extends SaslMechanism { public class Plain extends SaslMechanism {
public static final String MECHANISM = "PLAIN"; public static final String MECHANISM = "PLAIN";
public Plain(final TagWriter tagWriter, final Account account) { public Plain(final Account account) {
super(tagWriter, account, null); super(account);
}
public static String getMessage(String username, String password) {
final String message = '\u0000' + username + '\u0000' + password;
return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
} }
@Override @Override
@ -29,9 +33,4 @@ public class Plain extends SaslMechanism {
public String getClientFirstMessage() { public String getClientFirstMessage() {
return getMessage(account.getUsername(), account.getPassword()); return getMessage(account.getUsername(), account.getPassword());
} }
public static String getMessage(String username, String password) {
final String message = '\u0000' + username + '\u0000' + password;
return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
}
} }

View file

@ -2,18 +2,38 @@ package eu.siacs.conversations.crypto.sasl;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import java.security.SecureRandom; import java.util.Collection;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xml.TagWriter;
public abstract class SaslMechanism { public abstract class SaslMechanism {
final protected TagWriter tagWriter; protected final Account account;
final protected Account account;
final protected SecureRandom rng; protected SaslMechanism(final Account account) {
this.account = account;
}
/**
* The priority is used to pin the authentication mechanism. If authentication fails, it MAY be
* retried with another mechanism of the same priority, but MUST NOT be tried with a mechanism
* of lower priority (to prevent downgrade attacks).
*
* @return An arbitrary int representing the priority
*/
public abstract int getPriority();
public abstract String getMechanism();
public String getClientFirstMessage() {
return "";
}
public String getResponse(final String challenge) throws AuthenticationException {
return "";
}
protected enum State { protected enum State {
INITIAL, INITIAL,
@ -22,6 +42,22 @@ public abstract class SaslMechanism {
VALID_SERVER_RESPONSE, VALID_SERVER_RESPONSE,
} }
public enum Version {
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");
}
}
}
public static class AuthenticationException extends Exception { public static class AuthenticationException extends Exception {
public AuthenticationException(final String message) { public AuthenticationException(final String message) {
super(message); super(message);
@ -46,42 +82,32 @@ public abstract class SaslMechanism {
} }
} }
public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { public static final class Factory {
this.tagWriter = tagWriter;
this.account = account;
this.rng = rng;
}
/** private final Account account;
* The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another
* mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade
* attacks).
*
* @return An arbitrary int representing the priority
*/
public abstract int getPriority();
public abstract String getMechanism(); public Factory(final Account account) {
this.account = account;
}
public String getClientFirstMessage() { public SaslMechanism of(final Collection<String> mechanisms) {
return ""; if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) {
} return new External(account);
} else if (mechanisms.contains(ScramSha512.MECHANISM)) {
public String getResponse(final String challenge) throws AuthenticationException { return new ScramSha512(account);
return ""; } else if (mechanisms.contains(ScramSha256.MECHANISM)) {
} return new ScramSha256(account);
} else if (mechanisms.contains(ScramSha1.MECHANISM)) {
public enum Version { return new ScramSha1(account);
SASL, SASL_2; } else if (mechanisms.contains(Plain.MECHANISM)
&& !account.getServer().equals("nimbuzz.com")) {
public static Version of(final Element element) { return new Plain(account);
switch ( Strings.nullToEmpty(element.getNamespace())) { } else if (mechanisms.contains(DigestMd5.MECHANISM)) {
case Namespace.SASL: return new DigestMd5(account);
return SASL; } else if (mechanisms.contains(Anonymous.MECHANISM)) {
case Namespace.SASL_2: return new Anonymous(account);
return SASL_2; } else {
default: return null;
throw new IllegalArgumentException("Unrecognized SASL namespace");
} }
} }
} }

View file

@ -12,78 +12,53 @@ import org.bouncycastle.crypto.params.KeyParameter;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xml.TagWriter;
abstract class ScramMechanism extends SaslMechanism { abstract class ScramMechanism extends SaslMechanism {
// TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage. // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to
private final static String GS2_HEADER = "n,,"; // indicate support and/or usage.
private static final String GS2_HEADER = "n,,";
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
private static final Cache<CacheKey, KeyPair> CACHE =
protected abstract HMac getHMAC(); CacheBuilder.newBuilder().maximumSize(10).build();
protected abstract Digest getDigest();
private static final Cache<CacheKey, KeyPair> CACHE = CacheBuilder.newBuilder().maximumSize(10).build();
private static class CacheKey {
final String algorithm;
final String password;
final String salt;
final int iterations;
private CacheKey(String algorithm, String password, String salt, int iterations) {
this.algorithm = algorithm;
this.password = password;
this.salt = salt;
this.iterations = iterations;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheKey cacheKey = (CacheKey) o;
return iterations == cacheKey.iterations &&
Objects.equal(algorithm, cacheKey.algorithm) &&
Objects.equal(password, cacheKey.password) &&
Objects.equal(salt, cacheKey.salt);
}
@Override
public int hashCode() {
return Objects.hashCode(algorithm, password, salt, iterations);
}
}
private KeyPair getKeyPair(final String password, final String salt, final int iterations) throws ExecutionException {
return CACHE.get(new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), () -> {
final byte[] saltedPassword, serverKey, clientKey;
saltedPassword = hi(password.getBytes(), Base64.decode(salt, Base64.DEFAULT), iterations);
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
return new KeyPair(clientKey, serverKey);
});
}
private final String clientNonce; private final String clientNonce;
protected State state = State.INITIAL; protected State state = State.INITIAL;
private String clientFirstMessageBare; private String clientFirstMessageBare;
private byte[] serverSignature = null; private byte[] serverSignature = null;
ScramMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { ScramMechanism(final Account account) {
super(tagWriter, account, rng); super(account);
// This nonce should be different for each authentication attempt. // This nonce should be different for each authentication attempt.
clientNonce = CryptoHelper.random(100, rng); this.clientNonce = CryptoHelper.random(100);
clientFirstMessageBare = ""; clientFirstMessageBare = "";
} }
protected abstract HMac getHMAC();
protected abstract Digest getDigest();
private KeyPair getKeyPair(final String password, final String salt, final int iterations)
throws ExecutionException {
return CACHE.get(
new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations),
() -> {
final byte[] saltedPassword, serverKey, clientKey;
saltedPassword =
hi(
password.getBytes(),
Base64.decode(salt, Base64.DEFAULT),
iterations);
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
return new KeyPair(clientKey, serverKey);
});
}
private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException { private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException {
final HMac hMac = getHMAC(); final HMac hMac = getHMAC();
hMac.init(new KeyParameter(key)); hMac.init(new KeyParameter(key));
@ -123,8 +98,11 @@ abstract class ScramMechanism extends SaslMechanism {
@Override @Override
public String getClientFirstMessage() { public String getClientFirstMessage() {
if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + clientFirstMessageBare =
",r=" + this.clientNonce; "n="
+ CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername()))
+ ",r="
+ this.clientNonce;
state = State.AUTH_TEXT_SENT; state = State.AUTH_TEXT_SENT;
} }
return Base64.encodeToString( return Base64.encodeToString(
@ -173,7 +151,8 @@ abstract class ScramMechanism extends SaslMechanism {
* MUST cause authentication failure when the attribute is parsed by * MUST cause authentication failure when the attribute is parsed by
* the other end. * the other end.
*/ */
throw new AuthenticationException("Server sent reserved token: `m'"); throw new AuthenticationException(
"Server sent reserved token: `m'");
} }
} }
} }
@ -182,20 +161,33 @@ abstract class ScramMechanism extends SaslMechanism {
throw new AuthenticationException("Server did not send iteration count"); throw new AuthenticationException("Server did not send iteration count");
} }
if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) {
throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); throw new AuthenticationException(
"Server nonce does not contain client nonce: " + nonce);
} }
if (salt.isEmpty()) { if (salt.isEmpty()) {
throw new AuthenticationException("Server sent empty salt"); throw new AuthenticationException("Server sent empty salt");
} }
final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( final String clientFinalMessageWithoutProof =
GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; "c="
final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' + Base64.encodeToString(GS2_HEADER.getBytes(), Base64.NO_WRAP)
+ clientFinalMessageWithoutProof).getBytes(); + ",r="
+ nonce;
final byte[] authMessage =
(clientFirstMessageBare
+ ','
+ new String(serverFirstMessage)
+ ','
+ clientFinalMessageWithoutProof)
.getBytes();
final KeyPair keys; final KeyPair keys;
try { try {
keys = getKeyPair(CryptoHelper.saslPrep(account.getPassword()), salt, iterationCount); keys =
getKeyPair(
CryptoHelper.saslPrep(account.getPassword()),
salt,
iterationCount);
} catch (ExecutionException e) { } catch (ExecutionException e) {
throw new AuthenticationException("Invalid keys generated"); throw new AuthenticationException("Invalid keys generated");
} }
@ -213,35 +205,69 @@ abstract class ScramMechanism extends SaslMechanism {
final byte[] clientProof = new byte[keys.clientKey.length]; final byte[] clientProof = new byte[keys.clientKey.length];
if (clientSignature.length < keys.clientKey.length) { if (clientSignature.length < keys.clientKey.length) {
throw new AuthenticationException("client signature was shorter than clientKey"); throw new AuthenticationException(
"client signature was shorter than clientKey");
} }
for (int i = 0; i < clientProof.length; i++) { for (int i = 0; i < clientProof.length; i++) {
clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]);
} }
final String clientFinalMessage =
final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + clientFinalMessageWithoutProof
Base64.encodeToString(clientProof, Base64.NO_WRAP); + ",p="
+ Base64.encodeToString(clientProof, Base64.NO_WRAP);
state = State.RESPONSE_SENT; state = State.RESPONSE_SENT;
return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP);
case RESPONSE_SENT: case RESPONSE_SENT:
try { try {
final String clientCalculatedServerFinalMessage = "v=" + final String clientCalculatedServerFinalMessage =
Base64.encodeToString(serverSignature, Base64.NO_WRAP); "v=" + Base64.encodeToString(serverSignature, Base64.NO_WRAP);
if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { if (!clientCalculatedServerFinalMessage.equals(
new String(Base64.decode(challenge, Base64.DEFAULT)))) {
throw new Exception(); throw new Exception();
} }
state = State.VALID_SERVER_RESPONSE; state = State.VALID_SERVER_RESPONSE;
return ""; return "";
} catch (Exception e) { } catch (Exception e) {
throw new AuthenticationException("Server final message does not match calculated final message"); throw new AuthenticationException(
"Server final message does not match calculated final message");
} }
default: default:
throw new InvalidStateException(state); throw new InvalidStateException(state);
} }
} }
private static class CacheKey {
final String algorithm;
final String password;
final String salt;
final int iterations;
private CacheKey(String algorithm, String password, String salt, int iterations) {
this.algorithm = algorithm;
this.password = password;
this.salt = salt;
this.iterations = iterations;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheKey cacheKey = (CacheKey) o;
return iterations == cacheKey.iterations
&& Objects.equal(algorithm, cacheKey.algorithm)
&& Objects.equal(password, cacheKey.password)
&& Objects.equal(salt, cacheKey.salt);
}
@Override
public int hashCode() {
return Objects.hashCode(algorithm, password, salt, iterations);
}
}
private static class KeyPair { private static class KeyPair {
final byte[] clientKey; final byte[] clientKey;
final byte[] serverKey; final byte[] serverKey;

View file

@ -4,15 +4,16 @@ import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.macs.HMac;
import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter;
public class ScramSha1 extends ScramMechanism { public class ScramSha1 extends ScramMechanism {
public static final String MECHANISM = "SCRAM-SHA-1"; public static final String MECHANISM = "SCRAM-SHA-1";
public ScramSha1(final Account account) {
super(account);
}
@Override @Override
protected HMac getHMAC() { protected HMac getHMAC() {
return new HMac(new SHA1Digest()); return new HMac(new SHA1Digest());
@ -23,10 +24,6 @@ public class ScramSha1 extends ScramMechanism {
return new SHA1Digest(); return new SHA1Digest();
} }
public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
super(tagWriter, account, rng);
}
@Override @Override
public int getPriority() { public int getPriority() {
return 20; return 20;

View file

@ -4,15 +4,16 @@ import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.macs.HMac;
import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter;
public class ScramSha256 extends ScramMechanism { public class ScramSha256 extends ScramMechanism {
public static final String MECHANISM = "SCRAM-SHA-256"; public static final String MECHANISM = "SCRAM-SHA-256";
public ScramSha256(final Account account) {
super(account);
}
@Override @Override
protected HMac getHMAC() { protected HMac getHMAC() {
return new HMac(new SHA256Digest()); return new HMac(new SHA256Digest());
@ -23,10 +24,6 @@ public class ScramSha256 extends ScramMechanism {
return new SHA256Digest(); return new SHA256Digest();
} }
public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
super(tagWriter, account, rng);
}
@Override @Override
public int getPriority() { public int getPriority() {
return 25; return 25;

View file

@ -4,15 +4,16 @@ import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA512Digest; import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.macs.HMac;
import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter;
public class ScramSha512 extends ScramMechanism { public class ScramSha512 extends ScramMechanism {
public static final String MECHANISM = "SCRAM-SHA-512"; public static final String MECHANISM = "SCRAM-SHA-512";
public ScramSha512(final Account account) {
super(account);
}
@Override @Override
protected HMac getHMAC() { protected HMac getHMAC() {
return new HMac(new SHA512Digest()); return new HMac(new SHA512Digest());
@ -23,10 +24,6 @@ public class ScramSha512 extends ScramMechanism {
return new SHA512Digest(); return new SHA512Digest();
} }
public ScramSha512(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
super(tagWriter, account, rng);
}
@Override @Override
public int getPriority() { public int getPriority() {
return 30; return 30;

View file

@ -6,9 +6,7 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
/** /** A tokenizer for GS2 header strings */
* A tokenizer for GS2 header strings
*/
public final class Tokenizer implements Iterator<String>, Iterable<String> { public final class Tokenizer implements Iterator<String>, Iterable<String> {
private final List<String> parts; private final List<String> parts;
private int index; private int index;
@ -50,18 +48,19 @@ public final class Tokenizer implements Iterator<String>, Iterable<String> {
} }
/** /**
* Removes the last object returned by {@code next} from the collection. * Removes the last object returned by {@code next} from the collection. This method can only be
* This method can only be called once between each call to {@code next}. * called once between each call to {@code next}.
* *
* @throws UnsupportedOperationException if removing is not supported by the collection being * @throws UnsupportedOperationException if removing is not supported by the collection being
* iterated. * iterated.
* @throws IllegalStateException if {@code next} has not been called, or {@code remove} has * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has
* already been called after the last call to {@code next}. * already been called after the last call to {@code next}.
*/ */
@Override @Override
public void remove() { public void remove() {
if (index <= 0) { if (index <= 0) {
throw new IllegalStateException("You can't delete an element before first next() method call"); throw new IllegalStateException(
"You can't delete an element before first next() method call");
} }
parts.remove(--index); parts.remove(--index);
} }

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.http; package eu.siacs.conversations.http;
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.os.Build; import android.os.Build;
import android.util.Log; import android.util.Log;
@ -147,7 +149,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
trustManager = mXmppConnectionService.getMemorizingTrustManager().getNonInteractive(); trustManager = mXmppConnectionService.getMemorizingTrustManager().getNonInteractive();
} }
try { try {
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG()); final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, SECURE_RANDOM);
builder.sslSocketFactory(sf, trustManager); builder.sslSocketFactory(sf, trustManager);
builder.hostnameVerifier(new StrictHostnameVerifier()); builder.hostnameVerifier(new StrictHostnameVerifier());
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) { } catch (final KeyManagementException | NoSuchAlgorithmException ignored) {

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.http; package eu.siacs.conversations.http;
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -124,7 +126,7 @@ public class HttpUploadConnection implements Transferable, AbstractConnectionMan
|| message.getEncryption() == Message.ENCRYPTION_AXOLOTL || message.getEncryption() == Message.ENCRYPTION_AXOLOTL
|| message.getEncryption() == Message.ENCRYPTION_OTR) { || message.getEncryption() == Message.ENCRYPTION_OTR) {
this.key = new byte[44]; this.key = new byte[44];
mXmppConnectionService.getRNG().nextBytes(this.key); SECURE_RANDOM.nextBytes(this.key);
this.file.setKeyAndIv(this.key); this.file.setKeyAndIv(this.key);
} }
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0)); this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.services; package eu.siacs.conversations.services;
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.util.Log; import android.util.Log;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -502,7 +504,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
this.start = start.getTimestamp(); this.start = start.getTimestamp();
} }
this.end = end; this.end = end;
this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); this.queryId = new BigInteger(50, SECURE_RANDOM).toString(32);
this.version = version; this.version = version;
} }

View file

@ -1,6 +1,7 @@
package eu.siacs.conversations.services; package eu.siacs.conversations.services;
import static eu.siacs.conversations.utils.Compatibility.s; import static eu.siacs.conversations.utils.Compatibility.s;
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@ -379,7 +380,6 @@ public class XmppConnectionService extends Service {
} }
}; };
private final AtomicLong mLastExpiryRun = new AtomicLong(0); private final AtomicLong mLastExpiryRun = new AtomicLong(0);
private SecureRandom mRandom;
private final LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache = new LruCache<>(20); private final LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache = new LruCache<>(20);
private final OnStatusChanged statusListener = new OnStatusChanged() { private final OnStatusChanged statusListener = new OnStatusChanged() {
@ -451,7 +451,7 @@ public class XmppConnectionService extends Service {
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": went into offline state during low ping mode. reconnecting now"); Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": went into offline state during low ping mode. reconnecting now");
reconnectAccount(account, true, false); reconnectAccount(account, true, false);
} else { } else {
int timeToReconnect = mRandom.nextInt(10) + 2; final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2;
scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
} }
} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) { } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
@ -1143,7 +1143,6 @@ public class XmppConnectionService extends Service {
Log.e(Config.LOGTAG, "unable to initialize security provider", throwable); Log.e(Config.LOGTAG, "unable to initialize security provider", throwable);
} }
Resolver.init(this); Resolver.init(this);
this.mRandom = new SecureRandom();
updateMemorizingTrustmanager(); updateMemorizingTrustmanager();
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8; final int cacheSize = maxMemory / 8;
@ -3269,7 +3268,7 @@ public class XmppConnectionService extends Service {
} }
return false; return false;
} }
final Jid jid = Jid.of(CryptoHelper.pronounceable(getRNG()), server, null); final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null);
final Conversation conversation = findOrCreateConversation(account, jid, true, false, true); final Conversation conversation = findOrCreateConversation(account, jid, true, false, true);
joinMuc(conversation, new OnConferenceJoined() { joinMuc(conversation, new OnConferenceJoined() {
@Override @Override
@ -4366,10 +4365,6 @@ public class XmppConnectionService extends Service {
} }
} }
public SecureRandom getRNG() {
return this.mRandom;
}
public MemorizingTrustManager getMemorizingTrustManager() { public MemorizingTrustManager getMemorizingTrustManager() {
return this.mMemorizingTrustManager; return this.mMemorizingTrustManager;
} }

View file

@ -43,7 +43,6 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
private boolean jidWasModified = false; private boolean jidWasModified = false;
private boolean nameEntered = false; private boolean nameEntered = false;
private boolean skipTetxWatcher = false; private boolean skipTetxWatcher = false;
private static final SecureRandom RANDOM = new SecureRandom();
public static CreatePublicChannelDialog newInstance(List<String> accounts) { public static CreatePublicChannelDialog newInstance(List<String> accounts) {
CreatePublicChannelDialog dialog = new CreatePublicChannelDialog(); CreatePublicChannelDialog dialog = new CreatePublicChannelDialog();
@ -158,7 +157,7 @@ public class CreatePublicChannelDialog extends DialogFragment implements OnBacke
try { try {
return Jid.of(localpart, domain, null).toEscapedString(); return Jid.of(localpart, domain, null).toEscapedString();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return Jid.of(CryptoHelper.pronounceable(RANDOM), domain, null).toEscapedString(); return Jid.of(CryptoHelper.pronounceable(), domain, null).toEscapedString();
} }
} }
} }

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.utils; package eu.siacs.conversations.utils;
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.os.Bundle; import android.os.Bundle;
import android.util.Base64; import android.util.Base64;
import android.util.Pair; import android.util.Pair;
@ -59,12 +61,12 @@ public final class CryptoHelper {
return builder.toString(); return builder.toString();
} }
public static String pronounceable(SecureRandom random) { public static String pronounceable() {
final int rand = random.nextInt(4); final int rand = SECURE_RANDOM.nextInt(4);
char[] output = new char[rand * 2 + (5 - rand)]; char[] output = new char[rand * 2 + (5 - rand)];
boolean vowel = random.nextBoolean(); boolean vowel = SECURE_RANDOM.nextBoolean();
for (int i = 0; i < output.length; ++i) { for (int i = 0; i < output.length; ++i) {
output[i] = vowel ? VOWELS[random.nextInt(VOWELS.length)] : CONSONANTS[random.nextInt(CONSONANTS.length)]; output[i] = vowel ? VOWELS[SECURE_RANDOM.nextInt(VOWELS.length)] : CONSONANTS[SECURE_RANDOM.nextInt(CONSONANTS.length)];
vowel = !vowel; vowel = !vowel;
} }
return String.valueOf(output); return String.valueOf(output);
@ -117,9 +119,9 @@ public final class CryptoHelper {
return Normalizer.normalize(s, Normalizer.Form.NFKC); return Normalizer.normalize(s, Normalizer.Form.NFKC);
} }
public static String random(int length, SecureRandom random) { public static String random(final int length) {
final byte[] bytes = new byte[length]; final byte[] bytes = new byte[length];
random.nextBytes(bytes); SECURE_RANDOM.nextBytes(bytes);
return Base64.encodeToString(bytes, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE); return Base64.encodeToString(bytes, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE);
} }

View file

@ -0,0 +1,13 @@
package eu.siacs.conversations.utils;
import java.security.SecureRandom;
public final class Random {
public static final SecureRandom SECURE_RANDOM = new SecureRandom();
private Random() {
}
}

View file

@ -1,5 +1,7 @@
package eu.siacs.conversations.xmpp; package eu.siacs.conversations.xmpp;
import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
@ -521,7 +523,7 @@ public class XmppConnection implements Runnable {
? trustManager.getInteractive(domain) ? trustManager.getInteractive(domain)
: trustManager.getNonInteractive(domain) : trustManager.getNonInteractive(domain)
}, },
mXmppConnectionService.getRNG()); SECURE_RANDOM);
return sc.getSocketFactory(); return sc.getSocketFactory();
} }
@ -1216,23 +1218,11 @@ 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 Element element = streamFeatures.findChild("mechanisms");
if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { final Collection<String> mechanisms = Collections2.transform(element.getChildren(), c -> c == null ? null : c.getContent());
saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); final SaslMechanism.Factory factory = new SaslMechanism.Factory(account);
} else if (mechanisms.contains(ScramSha512.MECHANISM)) { this.saslMechanism = factory.of(mechanisms);
saslMechanism = new ScramSha512(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains(ScramSha256.MECHANISM)) {
saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains(ScramSha1.MECHANISM)) {
saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains(Plain.MECHANISM)
&& !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) {
saslMechanism = new Plain(tagWriter, account);
} else if (mechanisms.contains(DigestMd5.MECHANISM)) {
saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains(Anonymous.MECHANISM)) {
saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG());
}
if (saslMechanism == null) { if (saslMechanism == null) {
Log.d( Log.d(
Config.LOGTAG, Config.LOGTAG,
@ -1317,12 +1307,8 @@ public class XmppConnection implements Runnable {
return bind; return bind;
} }
private static List<String> extractMechanisms(final Element stream) { private static Collection<String> extractMechanisms(final Element stream) {
final ArrayList<String> mechanisms = new ArrayList<>(stream.getChildren().size()); return Collections2.transform(stream.getChildren(), c -> c == null ? null : c.getContent());
for (final Element child : stream.getChildren()) {
mechanisms.add(child.getContent());
}
return mechanisms;
} }
private void register() { private void register() {
@ -1963,8 +1949,8 @@ public class XmppConnection implements Runnable {
return nextRandomId(false); return nextRandomId(false);
} }
private String nextRandomId(boolean s) { private String nextRandomId(final boolean s) {
return CryptoHelper.random(s ? 3 : 9, mXmppConnectionService.getRNG()); return CryptoHelper.random(s ? 3 : 9);
} }
public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) { public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {