diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java b/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java index 14da9a1e9d..d01f9b8bf6 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SASLAuthentication.java @@ -343,6 +343,7 @@ public void authenticated(Success success) throws SmackException { if (success.getData() != null) { challengeReceived(success.getData(), true); } + currentMechanism.checkIfSuccessfulOrThrow(); authenticationSuccessful = true; // Wake up the thread that is waiting in the #authenticate method synchronized (this) { diff --git a/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java b/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java index ff59f37e67..889446106e 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/SmackInitialization.java @@ -29,6 +29,7 @@ import org.jivesoftware.smack.compression.Java7ZlibInputOutputStream; import org.jivesoftware.smack.initializer.SmackInitializer; +import org.jivesoftware.smack.sasl.core.SCRAMSHA1Mechanism; import org.jivesoftware.smack.util.FileUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -130,6 +131,8 @@ public final class SmackInitialization { // Ignore. } + SASLAuthentication.registerSASLMechanism(new SCRAMSHA1Mechanism()); + SmackConfiguration.smackInitialized = true; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLAnonymous.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLAnonymous.java index 2332ead59c..9ccc38a2c1 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLAnonymous.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLAnonymous.java @@ -55,4 +55,9 @@ public SASLAnonymous newInstance() { return new SASLAnonymous(); } + @Override + public void checkIfSuccessfulOrThrow() throws SmackException { + // SASL Anonymous is always successful :) + } + } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java index ae3bd0bb3c..39416a2103 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java @@ -21,6 +21,7 @@ import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.sasl.packet.SaslStreamElements.AuthMechanism; import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Response; +import org.jivesoftware.smack.util.StringTransformer; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.stringencoder.Base64; @@ -71,6 +72,22 @@ public abstract class SASLMechanism implements Comparable { public static final String GSSAPI = "GSSAPI"; public static final String PLAIN = "PLAIN"; + // TODO Remove once Smack's min Android API is 9, where java.text.Normalizer is available + private static StringTransformer saslPrepTransformer; + + /** + * Set the SASLPrep StringTransformer. + *

+ * A simple SASLPrep StringTransformer would be for example: java.text.Normalizer.normalize(string, Form.NFKC); + *

+ * + * @param stringTransformer set StringTransformer to use for SASLPrep. + * @see RFC 4013 - SASLprep: Stringprep Profile for User Names and Passwords + */ + public static void setSaslPrepTransformer(StringTransformer stringTransformer) { + saslPrepTransformer = stringTransformer; + } + protected XMPPConnection connection; /** @@ -238,6 +255,8 @@ public final int compareTo(SASLMechanism other) { public abstract int getPriority(); + public abstract void checkIfSuccessfulOrThrow() throws SmackException; + public SASLMechanism instanceForAuthentication(XMPPConnection connection) { SASLMechanism saslMechansim = newInstance(); saslMechansim.connection = connection; @@ -249,4 +268,19 @@ public SASLMechanism instanceForAuthentication(XMPPConnection connection) { protected static byte[] toBytes(String string) { return StringUtils.toBytes(string); } + + /** + * SASLprep the given String. + * + * @param string the String to sasl prep. + * @return the given String SASL preped + * @see RFC 4013 - SASLprep: Stringprep Profile for User Names and Passwords + */ + protected static String saslPrep(String string) { + StringTransformer stringTransformer = saslPrepTransformer; + if (stringTransformer != null) { + return stringTransformer.transform(string); + } + return string; + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/core/SCRAMSHA1Mechanism.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/core/SCRAMSHA1Mechanism.java new file mode 100644 index 0000000000..db265834ee --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/core/SCRAMSHA1Mechanism.java @@ -0,0 +1,346 @@ +/** + * + * Copyright 2014 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.sasl.core; + +import java.security.InvalidKeyException; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.security.auth.callback.CallbackHandler; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.sasl.SASLMechanism; +import org.jivesoftware.smack.util.ByteUtils; +import org.jivesoftware.smack.util.MAC; +import org.jivesoftware.smack.util.SHA1; +import org.jivesoftware.smack.util.stringencoder.Base64; +import org.jxmpp.util.cache.Cache; +import org.jxmpp.util.cache.LruCache; + +public class SCRAMSHA1Mechanism extends SASLMechanism { + + public static final String NAME = "SCRAM-SHA-1"; + + private static final int RANDOM_ASCII_BYTE_COUNT = 32; + private static final String DEFAULT_GS2_HEADER = "n,,"; + private static final byte[] CLIENT_KEY_BYTES = toBytes("Client Key"); + private static final byte[] SERVER_KEY_BYTES = toBytes("Server Key"); + private static final byte[] ONE = new byte[] { 0, 0, 0, 1 }; + + private static final SecureRandom RANDOM = new SecureRandom(); + + private static final Cache CACHE = new LruCache(10); + + private enum State { + INITIAL, + AUTH_TEXT_SENT, + RESPONSE_SENT, + VALID_SERVER_RESPONSE, + } + + /** + * The state of the this instance of SASL SCRAM-SHA1 authentication. + */ + private State state = State.INITIAL; + + /** + * The client's random ASCII which is used as nonce + */ + private String clientRandomAscii; + + private String clientFirstMessageBare; + private byte[] serverSignature; + + @Override + protected void authenticateInternal(CallbackHandler cbh) throws SmackException { + throw new UnsupportedOperationException("CallbackHandler not (yet) supported"); + } + + @Override + protected byte[] getAuthenticationText() throws SmackException { + clientRandomAscii = getRandomAscii(); + String saslPrepedAuthcId = saslPrep(authenticationId); + clientFirstMessageBare = "n=" + escape(saslPrepedAuthcId) + ",r=" + clientRandomAscii; + String clientFirstMessage = DEFAULT_GS2_HEADER + clientFirstMessageBare; + state = State.AUTH_TEXT_SENT; + return toBytes(clientFirstMessage); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public int getPriority() { + return 110; + } + + @Override + public SCRAMSHA1Mechanism newInstance() { + return new SCRAMSHA1Mechanism(); + } + + + @Override + public void checkIfSuccessfulOrThrow() throws SmackException { + if (state != State.VALID_SERVER_RESPONSE) { + throw new SmackException("SCRAM-SHA1 is missing valid server response"); + } + } + + @Override + protected byte[] evaluateChallenge(byte[] challenge) throws SmackException { + final String challengeString = new String(challenge); + switch (state) { + case AUTH_TEXT_SENT: + final String serverFirstMessage = challengeString; + Map attributes = parseAttributes(challengeString); + + // Handle server random ASCII (nonce) + String rvalue = attributes.get('r'); + if (rvalue == null) { + throw new SmackException("Server random ASCII is null"); + } + if (rvalue.length() <= clientRandomAscii.length()) { + throw new SmackException("Server random ASCII is shorter then client random ASCII"); + } + String receivedClientRandomAscii = rvalue.substring(0, clientRandomAscii.length()); + if (!receivedClientRandomAscii.equals(clientRandomAscii)) { + throw new SmackException("Received client random ASCII does not match client random ASCII"); + } + + // Handle iterations + int iterations; + String iterationsString = attributes.get('i'); + if (iterationsString == null) { + throw new SmackException("Iterations attribute not set"); + } + try { + iterations = Integer.parseInt(iterationsString); + } + catch (NumberFormatException e) { + throw new SmackException("Exception parsing iterations", e); + } + + // Handle salt + String salt = attributes.get('s'); + if (salt == null) { + throw new SmackException("SALT not send"); + } + + // Parsing and error checking is done, we can now begin to calculate the values + + // First the client-final-message-without-proof + String clientFinalMessageWithoutProof = "c=" + Base64.encode(DEFAULT_GS2_HEADER) + ",r=" + rvalue; + + // AuthMessage := client-first-message-bare + "," + server-first-message + "," + + // client-final-message-without-proof + byte[] authMessage = toBytes(clientFirstMessageBare + ',' + serverFirstMessage + ',' + + clientFinalMessageWithoutProof); + + // RFC 5802 § 5.1 "Note that a client implementation MAY cache ClientKey&ServerKey … for later reauthentication … + // as it is likely that the server is going to advertise the same salt value upon reauthentication." + final String cacheKey = password + ',' + salt; + byte[] serverKey, clientKey; + Keys keys = CACHE.get(cacheKey); + if (keys == null) { + // SaltedPassword := Hi(Normalize(password), salt, i) + byte[] saltedPassword = hi(saslPrep(password), Base64.decode(salt), iterations); + + // ServerKey := HMAC(SaltedPassword, "Server Key") + serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); + + // ServerSignature := HMAC(ServerKey, AuthMessage) + serverSignature = hmac(serverKey, authMessage); + + // ClientKey := HMAC(SaltedPassword, "Client Key") + clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); + + keys = new Keys(clientKey, serverKey); + CACHE.put(cacheKey, keys); + } + else { + serverKey = keys.serverKey; + clientKey = keys.clientKey; + } + + + // StoredKey := H(ClientKey) + byte[] storedKey = SHA1.bytes(clientKey); + + // ClientSignature := HMAC(StoredKey, AuthMessage) + byte[] clientSignature = hmac(storedKey, authMessage); + + // ClientProof := ClientKey XOR ClientSignature + byte[] clientProof = new byte[clientKey.length]; + for (int i = 0; i < clientProof.length; i++) { + clientProof[i] = (byte) (clientKey[i] ^ clientSignature[i]); + } + + String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + Base64.encodeToString(clientProof); + state = State.RESPONSE_SENT; + return toBytes(clientFinalMessage); + case RESPONSE_SENT: + String clientCalculatedServerFinalMessage = "v=" + Base64.encodeToString(serverSignature); + if (!clientCalculatedServerFinalMessage.equals(challengeString)) { + throw new SmackException("Server final message does not match calculated one"); + } + state = State.VALID_SERVER_RESPONSE; + break; + default: + throw new SmackException("Invalid state"); + } + return null; + } + + private static Map parseAttributes(String string) throws SmackException { + if (string.length() == 0) { + return Collections.emptyMap(); + } + + String[] keyValuePairs = string.split(","); + Map res = new HashMap(keyValuePairs.length, 1); + for (String keyValuePair : keyValuePairs) { + if (keyValuePair.length() < 3) { + throw new SmackException("Invalid Key-Value pair: " + keyValuePair); + } + char key = keyValuePair.charAt(0); + if (keyValuePair.charAt(1) != '=') { + throw new SmackException("Invalid Key-Value pair: " + keyValuePair); + } + String value = keyValuePair.substring(2); + res.put(key, value); + } + + return res; + } + + /** + * Generate random ASCII. + *

+ * This method is non-static and package-private for unit testing purposes. + *

+ * @return A String of 32 random printable ASCII characters. + */ + String getRandomAscii() { + int count = 0; + char[] randomAscii = new char[RANDOM_ASCII_BYTE_COUNT]; + while (count < RANDOM_ASCII_BYTE_COUNT) { + int r = RANDOM.nextInt(128); + char c = (char) r; + // RFC 5802 § 5.1 specifies 'r:' to exclude the ',' character and to be only printable ASCII characters + if (!isPrintableNonCommaAsciiChar(c)) { + continue; + } + randomAscii[count++] = c; + } + return new String(randomAscii); + } + + private static boolean isPrintableNonCommaAsciiChar(char c) { + if (c == ',') { + return false; + } + return c >= 32 && c < 127; + } + + /** + * Escapes usernames or passwords for SASL SCRAM-SHA1. + *

+ * According to RFC 5802 § 5.1 'n:' + * "The characters ',' or '=' in usernames are sent as '=2C' and '=3D' respectively." + *

+ * + * @param string + * @return the escaped string + */ + private static String escape(String string) { + StringBuilder sb = new StringBuilder((int) (string.length() * 1.1)); + for (int i = 0; i < string.length(); i++) { + char c = string.charAt(i); + switch (c) { + case ',': + sb.append("=2C"); + break; + case '=': + sb.append("=3D"); + break; + default: + sb.append(c); + break; + } + } + return sb.toString(); + } + + /** + * RFC 5802 § 2.2 HMAC(key, str) + * + * @param key + * @param str + * @return + * @throws SmackException + */ + private static byte[] hmac(byte[] key, byte[] str) throws SmackException { + try { + return MAC.hmacsha1(key, str); + } + catch (InvalidKeyException e) { + throw new SmackException(NAME + " HMAC-SHA1 Exception", e); + } + } + + /** + * RFC 5802 § 2.2 Hi(str, salt, i) + *

+ * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the pseudorandom function + * (PRF) and with dkLen == output length of HMAC() == output length of H(). + *

+ * + * @param str + * @param salt + * @param iterations + * @return + * @throws SmackException + */ + private static byte[] hi(String str, byte[] salt, int iterations) throws SmackException { + byte[] key = str.getBytes(); + // U1 := HMAC(str, salt + INT(1)) + byte[] u = hmac(key, ByteUtils.concact(salt, ONE)); + byte[] res = u.clone(); + for (int i = 1; i < iterations; i++) { + u = hmac(key, u); + for (int j = 0; j < u.length; j++) { + res[j] ^= u[j]; + } + } + return res; + } + + private static class Keys { + private final byte[] clientKey; + private final byte[] serverKey; + + public Keys(byte[] clientKey, byte[] serverKey) { + this.clientKey = clientKey; + this.serverKey = serverKey; + } + } +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStreamElements.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStreamElements.java index 8ecd0f1bfd..b7d44963b8 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStreamElements.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/packet/SaslStreamElements.java @@ -52,6 +52,14 @@ public XmlStringBuilder toXML() { xml.closeElement(ELEMENT); return xml; } + + public String getMechanism() { + return mechanism; + } + + public String getAuthenticationText() { + return authenticationText; + } } /** @@ -100,6 +108,10 @@ public XmlStringBuilder toXML() { xml.closeElement(ELEMENT); return xml; } + + public String getAuthenticationText() { + return authenticationText; + } } /** diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/MAC.java b/smack-core/src/main/java/org/jivesoftware/smack/util/MAC.java new file mode 100644 index 0000000000..4d78d24f8e --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/MAC.java @@ -0,0 +1,54 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +public class MAC { + + public static final String HMACSHA1 = "HmacSHA1"; + + private static Mac HMAC_SHA1; + + static { + try { + HMAC_SHA1 = Mac.getInstance(HMACSHA1); + } + catch (NoSuchAlgorithmException e) { + // Smack wont be able to function normally if this exception is thrown, wrap it into + // an ISE and make the user aware of the problem. + throw new IllegalStateException(e); + } + } + + + public static synchronized byte[] hmacsha1(SecretKeySpec key, byte[] input) throws InvalidKeyException { + HMAC_SHA1.init(key); + return HMAC_SHA1.doFinal(input); + } + + public static byte[] hmacsha1(byte[] keyBytes, byte[] input) throws InvalidKeyException { + SecretKeySpec key = new SecretKeySpec(keyBytes, HMACSHA1); + return hmacsha1(key, input); + } + + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/SHA1.java b/smack-core/src/main/java/org/jivesoftware/smack/util/SHA1.java new file mode 100644 index 0000000000..1dff5f1047 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/SHA1.java @@ -0,0 +1,57 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class SHA1 { + + /** + * Used by the hash method. + */ + private static MessageDigest SHA1_DIGEST; + + static { + try { + SHA1_DIGEST = MessageDigest.getInstance(StringUtils.SHA1); + } + catch (NoSuchAlgorithmException e) { + // Smack wont be able to function normally if this exception is thrown, wrap it into + // an ISE and make the user aware of the problem. + throw new IllegalStateException(e); + } + } + + public static synchronized byte[] bytes(byte[] bytes) { + SHA1_DIGEST.update(bytes); + return SHA1_DIGEST.digest(); + } + + public static byte[] bytes(String string) { + return bytes(StringUtils.toBytes(string)); + } + + public static String hex(byte[] bytes) { + return StringUtils.encodeHex(bytes(bytes)); + } + + public static String hex(String string) { + return hex(StringUtils.toBytes(string)); + } + +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java index f1267db7f4..8a3c5aeabe 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/StringUtils.java @@ -18,8 +18,6 @@ package org.jivesoftware.smack.util; import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.Random; @@ -100,11 +98,6 @@ public static CharSequence escapeForXML(final String string) { return out; } - /** - * Used by the hash method. - */ - private static MessageDigest digest = null; - /** * Hashes a String using the SHA-1 algorithm and returns the result as a * String of hexadecimal numbers. This method is synchronized to avoid @@ -120,21 +113,11 @@ public static CharSequence escapeForXML(final String string) { * * @param data the String to compute the hash of. * @return a hashed version of the passed-in String + * @deprecated use {@link org.jivesoftware.smack.util.SHA1#hex(String)} instead. */ + @Deprecated public synchronized static String hash(String data) { - if (digest == null) { - try { - digest = MessageDigest.getInstance(SHA1); - } - catch (NoSuchAlgorithmException nsae) { - // Smack wont be able to function normally if this exception is thrown, wrap it into - // an ISE and make the user aware of the problem. - throw new IllegalStateException(nsae); - } - } - // Now, compute hash. - digest.update(toBytes(data)); - return encodeHex(digest.digest()); + return org.jivesoftware.smack.util.SHA1.hex(data); } /** diff --git a/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java b/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java index 5689ead96e..09b1d09715 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/DummyConnection.java @@ -23,9 +23,9 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import org.jivesoftware.smack.packet.Element; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.PlainStreamElement; +import org.jivesoftware.smack.packet.TopLevelStreamElement; /** * A dummy implementation of {@link XMPPConnection}, intended to be used during @@ -53,7 +53,7 @@ public class DummyConnection extends AbstractXMPPConnection { private String connectionID; private Roster roster; - private final BlockingQueue queue = new LinkedBlockingQueue(); + private final BlockingQueue queue = new LinkedBlockingQueue(); public DummyConnection() { this(new ConnectionConfiguration("example.com")); @@ -211,8 +211,9 @@ public int getNumberOfSentPackets() { * @return a sent packet. * @throws InterruptedException */ - public Packet getSentPacket() throws InterruptedException { - return (Packet) queue.poll(); + @SuppressWarnings("unchecked") + public

P getSentPacket() throws InterruptedException { + return (P) queue.poll(); } /** @@ -224,8 +225,9 @@ public Packet getSentPacket() throws InterruptedException { * @return a sent packet. * @throws InterruptedException */ - public Packet getSentPacket(int wait) throws InterruptedException { - return (Packet) queue.poll(wait, TimeUnit.SECONDS); + @SuppressWarnings("unchecked") + public

P getSentPacket(int wait) throws InterruptedException { + return (P) queue.poll(wait, TimeUnit.SECONDS); } /** diff --git a/smack-core/src/test/java/org/jivesoftware/smack/sasl/core/SCRAMSHA1MechanismTest.java b/smack-core/src/test/java/org/jivesoftware/smack/sasl/core/SCRAMSHA1MechanismTest.java new file mode 100644 index 0000000000..fe8c808a98 --- /dev/null +++ b/smack-core/src/test/java/org/jivesoftware/smack/sasl/core/SCRAMSHA1MechanismTest.java @@ -0,0 +1,72 @@ +/** + * + * Copyright 2014 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.sasl.core; + +import static org.junit.Assert.assertEquals; + +import org.jivesoftware.smack.DummyConnection; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.AuthMechanism; +import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Response; +import org.jivesoftware.smack.test.util.SmackTestSuite; +import org.jivesoftware.smack.util.stringencoder.Base64; +import org.junit.Before; +import org.junit.Test; + +public class SCRAMSHA1MechanismTest { + + public static final String USERNAME = "user"; + public static final String PASSWORD = "pencil"; + public static final String CLIENT_FIRST_MESSAGE = "n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL"; + public static final String SERVER_FIRST_MESSAGE = "r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096"; + public static final String CLIENT_FINAL_MESSAGE = "c=biws,r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,p=v0X8v3Bz2T0CJGbJQyF0X+HI4Ts="; + public static final String SERVER_FINAL_MESSAGE = "v=rmF9pqV8S7suAoZWja4dJRkFsKQ="; + + @Before + public void init() { + SmackTestSuite.init(); + } + + @Test + public void testScramSha1Mechanism() throws NotConnectedException, SmackException, InterruptedException { + final DummyConnection con = new DummyConnection(); + SCRAMSHA1Mechanism mech = new SCRAMSHA1Mechanism() { + @Override + public String getRandomAscii() { + this.connection = con; + return "fyko+d2lbbFgONRv9qkxdawL"; + } + }; + + mech.authenticate(USERNAME, "unusedFoo", "unusedBar", PASSWORD); + AuthMechanism authMechanism = con.getSentPacket(); + assertEquals(SCRAMSHA1Mechanism.NAME, authMechanism.getMechanism()); + assertEquals(CLIENT_FIRST_MESSAGE, saslLayerString(authMechanism.getAuthenticationText())); + + mech.challengeReceived(Base64.encode(SERVER_FIRST_MESSAGE), false); + Response response = con.getSentPacket(); + assertEquals(CLIENT_FINAL_MESSAGE, saslLayerString(response.getAuthenticationText())); + + mech.challengeReceived(Base64.encode(SERVER_FINAL_MESSAGE), true); + mech.checkIfSuccessfulOrThrow(); + } + + private static String saslLayerString(String string) { + return Base64.decodeToString(string); + } +} diff --git a/smack-core/src/test/java/org/jivesoftware/smack/util/SHA1Test.java b/smack-core/src/test/java/org/jivesoftware/smack/util/SHA1Test.java new file mode 100644 index 0000000000..b9fc3818d5 --- /dev/null +++ b/smack-core/src/test/java/org/jivesoftware/smack/util/SHA1Test.java @@ -0,0 +1,82 @@ +/** + * + * Copyright 2003-2007 Jive Software. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smack.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +/** + * A test case for the SHA1 class. + */ +public class SHA1Test { + + @Test + public void testHash() { + // Test null + // @TODO - should the StringUtils.hash(String) method be fixed to handle null input? + try { + SHA1.hex((String) null); + fail(); + } + catch (NullPointerException npe) { + assertTrue(true); + } + + // Test empty String + String result = SHA1.hex(""); + assertEquals("da39a3ee5e6b4b0d3255bfef95601890afd80709", result); + + // Test a known hash + String adminInHash = "d033e22ae348aeb5660fc2140aec35850c4da997"; + result = SHA1.hex("admin"); + assertEquals(adminInHash, result); + + // Test a random String - make sure all resulting characters are valid hash characters + // and that the returned string is 32 characters long. + String random = "jive software blah and stuff this is pretty cool"; + result = SHA1.hex(random); + assertTrue(isValidHash(result)); + + // Test junk input: + String junk = "\n\n\t\b\r!@(!)^(#)@+_-\u2031\u09291\u00A9\u00BD\u0394\u00F8"; + result = SHA1.hex(junk); + assertTrue(isValidHash(result)); + } + + /* ----- Utility methods and vars ----- */ + + private final String HASH_CHARS = "0123456789abcdef"; + + /** + * Returns true if the input string is valid md5 hash, false otherwise. + */ + private boolean isValidHash(String result) { + boolean valid = true; + for (int i=0; i