SCRAMSHA1Mechanism.java

  1. /**
  2.  *
  3.  * Copyright 2014 Florian Schmaus
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smack.sasl.core;

  18. import java.security.InvalidKeyException;
  19. import java.security.SecureRandom;
  20. import java.util.Collections;
  21. import java.util.HashMap;
  22. import java.util.Map;

  23. import javax.security.auth.callback.CallbackHandler;

  24. import org.jivesoftware.smack.SmackException;
  25. import org.jivesoftware.smack.sasl.SASLMechanism;
  26. import org.jivesoftware.smack.util.ByteUtils;
  27. import org.jivesoftware.smack.util.MAC;
  28. import org.jivesoftware.smack.util.SHA1;
  29. import org.jivesoftware.smack.util.stringencoder.Base64;
  30. import org.jxmpp.util.cache.Cache;
  31. import org.jxmpp.util.cache.LruCache;

  32. public class SCRAMSHA1Mechanism extends SASLMechanism {

  33.     public static final String NAME = "SCRAM-SHA-1";

  34.     private static final int RANDOM_ASCII_BYTE_COUNT = 32;
  35.     private static final String DEFAULT_GS2_HEADER = "n,,";
  36.     private static final byte[] CLIENT_KEY_BYTES = toBytes("Client Key");
  37.     private static final byte[] SERVER_KEY_BYTES = toBytes("Server Key");
  38.     private static final byte[] ONE = new byte[] { 0, 0, 0, 1 };

  39.     private static final SecureRandom RANDOM = new SecureRandom();

  40.     private static final Cache<String, Keys> CACHE = new LruCache<String, Keys>(10);

  41.     private enum State {
  42.         INITIAL,
  43.         AUTH_TEXT_SENT,
  44.         RESPONSE_SENT,
  45.         VALID_SERVER_RESPONSE,
  46.     }

  47.     /**
  48.      * The state of the this instance of SASL SCRAM-SHA1 authentication.
  49.      */
  50.     private State state = State.INITIAL;

  51.     /**
  52.      * The client's random ASCII which is used as nonce
  53.      */
  54.     private String clientRandomAscii;

  55.     private String clientFirstMessageBare;
  56.     private byte[] serverSignature;

  57.     @Override
  58.     protected void authenticateInternal(CallbackHandler cbh) throws SmackException {
  59.         throw new UnsupportedOperationException("CallbackHandler not (yet) supported");
  60.     }

  61.     @Override
  62.     protected byte[] getAuthenticationText() throws SmackException {
  63.         clientRandomAscii = getRandomAscii();
  64.         String saslPrepedAuthcId = saslPrep(authenticationId);
  65.         clientFirstMessageBare = "n=" + escape(saslPrepedAuthcId) + ",r=" + clientRandomAscii;
  66.         String clientFirstMessage = DEFAULT_GS2_HEADER + clientFirstMessageBare;
  67.         state = State.AUTH_TEXT_SENT;
  68.         return toBytes(clientFirstMessage);
  69.     }

  70.     @Override
  71.     public String getName() {
  72.         return NAME;
  73.     }

  74.     @Override
  75.     public int getPriority() {
  76.         return 110;
  77.     }

  78.     @Override
  79.     public SCRAMSHA1Mechanism newInstance() {
  80.         return new SCRAMSHA1Mechanism();
  81.     }


  82.     @Override
  83.     public void checkIfSuccessfulOrThrow() throws SmackException {
  84.         if (state != State.VALID_SERVER_RESPONSE) {
  85.             throw new SmackException("SCRAM-SHA1 is missing valid server response");
  86.         }
  87.     }

  88.     @Override
  89.     protected byte[] evaluateChallenge(byte[] challenge) throws SmackException {
  90.         final String challengeString = new String(challenge);
  91.         switch (state) {
  92.         case AUTH_TEXT_SENT:
  93.             final String serverFirstMessage = challengeString;
  94.             Map<Character, String> attributes = parseAttributes(challengeString);

  95.             // Handle server random ASCII (nonce)
  96.             String rvalue = attributes.get('r');
  97.             if (rvalue == null) {
  98.                 throw new SmackException("Server random ASCII is null");
  99.             }
  100.             if (rvalue.length() <= clientRandomAscii.length()) {
  101.                 throw new SmackException("Server random ASCII is shorter then client random ASCII");
  102.             }
  103.             String receivedClientRandomAscii = rvalue.substring(0, clientRandomAscii.length());
  104.             if (!receivedClientRandomAscii.equals(clientRandomAscii)) {
  105.                 throw new SmackException("Received client random ASCII does not match client random ASCII");
  106.             }

  107.             // Handle iterations
  108.             int iterations;
  109.             String iterationsString = attributes.get('i');
  110.             if (iterationsString == null) {
  111.                 throw new SmackException("Iterations attribute not set");
  112.             }
  113.             try {
  114.                 iterations = Integer.parseInt(iterationsString);
  115.             }
  116.             catch (NumberFormatException e) {
  117.                 throw new SmackException("Exception parsing iterations", e);
  118.             }

  119.             // Handle salt
  120.             String salt = attributes.get('s');
  121.             if (salt == null) {
  122.                 throw new SmackException("SALT not send");
  123.             }

  124.             // Parsing and error checking is done, we can now begin to calculate the values

  125.             // First the client-final-message-without-proof
  126.             String clientFinalMessageWithoutProof = "c=" + Base64.encode(DEFAULT_GS2_HEADER) + ",r=" + rvalue;

  127.             // AuthMessage := client-first-message-bare + "," + server-first-message + "," +
  128.             // client-final-message-without-proof
  129.             byte[] authMessage = toBytes(clientFirstMessageBare + ',' + serverFirstMessage + ','
  130.                             + clientFinalMessageWithoutProof);

  131.             // RFC 5802 § 5.1 "Note that a client implementation MAY cache ClientKey&ServerKey … for later reauthentication …
  132.             // as it is likely that the server is going to advertise the same salt value upon reauthentication."
  133.             final String cacheKey = password + ',' + salt;
  134.             byte[] serverKey, clientKey;
  135.             Keys keys = CACHE.get(cacheKey);
  136.             if (keys == null) {
  137.                 // SaltedPassword := Hi(Normalize(password), salt, i)
  138.                 byte[] saltedPassword = hi(saslPrep(password), Base64.decode(salt), iterations);

  139.                 // ServerKey := HMAC(SaltedPassword, "Server Key")
  140.                 serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);

  141.                 // ClientKey := HMAC(SaltedPassword, "Client Key")
  142.                 clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);

  143.                 keys = new Keys(clientKey, serverKey);
  144.                 CACHE.put(cacheKey, keys);
  145.             }
  146.             else {
  147.                 serverKey = keys.serverKey;
  148.                 clientKey = keys.clientKey;
  149.             }

  150.             // ServerSignature := HMAC(ServerKey, AuthMessage)
  151.             serverSignature = hmac(serverKey, authMessage);

  152.             // StoredKey := H(ClientKey)
  153.             byte[] storedKey = SHA1.bytes(clientKey);

  154.             // ClientSignature := HMAC(StoredKey, AuthMessage)
  155.             byte[] clientSignature = hmac(storedKey, authMessage);

  156.             // ClientProof := ClientKey XOR ClientSignature
  157.             byte[] clientProof = new byte[clientKey.length];
  158.             for (int i = 0; i < clientProof.length; i++) {
  159.                 clientProof[i] = (byte) (clientKey[i] ^ clientSignature[i]);
  160.             }

  161.             String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + Base64.encodeToString(clientProof);
  162.             state = State.RESPONSE_SENT;
  163.             return toBytes(clientFinalMessage);
  164.         case RESPONSE_SENT:
  165.             String clientCalculatedServerFinalMessage = "v=" + Base64.encodeToString(serverSignature);
  166.             if (!clientCalculatedServerFinalMessage.equals(challengeString)) {
  167.                 throw new SmackException("Server final message does not match calculated one");
  168.             }
  169.             state = State.VALID_SERVER_RESPONSE;
  170.             break;
  171.         default:
  172.             throw new SmackException("Invalid state");
  173.         }
  174.         return null;
  175.     }

  176.     private static Map<Character, String> parseAttributes(String string) throws SmackException {
  177.         if (string.length() == 0) {
  178.             return Collections.emptyMap();
  179.         }

  180.         String[] keyValuePairs = string.split(",");
  181.         Map<Character, String> res = new HashMap<Character, String>(keyValuePairs.length, 1);
  182.         for (String keyValuePair : keyValuePairs) {
  183.             if (keyValuePair.length() < 3) {
  184.                 throw new SmackException("Invalid Key-Value pair: " + keyValuePair);
  185.             }
  186.             char key = keyValuePair.charAt(0);
  187.             if (keyValuePair.charAt(1) != '=') {
  188.                 throw new SmackException("Invalid Key-Value pair: " + keyValuePair);
  189.             }
  190.             String value = keyValuePair.substring(2);
  191.             res.put(key, value);
  192.         }

  193.         return res;
  194.     }

  195.     /**
  196.      * Generate random ASCII.
  197.      * <p>
  198.      * This method is non-static and package-private for unit testing purposes.
  199.      * </p>
  200.      * @return A String of 32 random printable ASCII characters.
  201.      */
  202.     String getRandomAscii() {
  203.         int count = 0;
  204.         char[] randomAscii = new char[RANDOM_ASCII_BYTE_COUNT];
  205.         while (count < RANDOM_ASCII_BYTE_COUNT) {
  206.             int r = RANDOM.nextInt(128);
  207.             char c = (char) r;
  208.             // RFC 5802 § 5.1 specifies 'r:' to exclude the ',' character and to be only printable ASCII characters
  209.             if (!isPrintableNonCommaAsciiChar(c)) {
  210.                 continue;
  211.             }
  212.             randomAscii[count++] = c;
  213.         }
  214.         return new String(randomAscii);
  215.     }

  216.     private static boolean isPrintableNonCommaAsciiChar(char c) {
  217.         if (c == ',') {
  218.             return false;
  219.         }
  220.         return c >= 32 && c < 127;
  221.     }

  222.     /**
  223.      * Escapes usernames or passwords for SASL SCRAM-SHA1.
  224.      * <p>
  225.      * According to RFC 5802 § 5.1 'n:'
  226.      * "The characters ',' or '=' in usernames are sent as '=2C' and '=3D' respectively."
  227.      * </p>
  228.      *
  229.      * @param string
  230.      * @return the escaped string
  231.      */
  232.     private static String escape(String string) {
  233.         StringBuilder sb = new StringBuilder((int) (string.length() * 1.1));
  234.         for (int i = 0; i < string.length(); i++) {
  235.             char c = string.charAt(i);
  236.             switch (c) {
  237.             case ',':
  238.                 sb.append("=2C");
  239.                 break;
  240.             case '=':
  241.                 sb.append("=3D");
  242.                 break;
  243.             default:
  244.                 sb.append(c);
  245.                 break;
  246.             }
  247.         }
  248.         return sb.toString();
  249.     }

  250.     /**
  251.      * RFC 5802 § 2.2 HMAC(key, str)
  252.      *
  253.      * @param key
  254.      * @param str
  255.      * @return the HMAC-SHA1 value of the input.
  256.      * @throws SmackException
  257.      */
  258.     private static byte[] hmac(byte[] key, byte[] str) throws SmackException {
  259.         try {
  260.             return MAC.hmacsha1(key, str);
  261.         }
  262.         catch (InvalidKeyException e) {
  263.             throw new SmackException(NAME + " HMAC-SHA1 Exception", e);
  264.         }
  265.     }

  266.     /**
  267.      * RFC 5802 § 2.2 Hi(str, salt, i)
  268.      * <p>
  269.      * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the pseudorandom function
  270.      * (PRF) and with dkLen == output length of HMAC() == output length of H().
  271.      * </p>
  272.      *
  273.      * @param str
  274.      * @param salt
  275.      * @param iterations
  276.      * @return the result of the Hi function.
  277.      * @throws SmackException
  278.      */
  279.     private static byte[] hi(String str, byte[] salt, int iterations) throws SmackException {
  280.         byte[] key = str.getBytes();
  281.         // U1 := HMAC(str, salt + INT(1))
  282.         byte[] u = hmac(key, ByteUtils.concact(salt, ONE));
  283.         byte[] res = u.clone();
  284.         for (int i = 1; i < iterations; i++) {
  285.             u = hmac(key, u);
  286.             for (int j = 0; j < u.length; j++) {
  287.                 res[j] ^= u[j];
  288.             }
  289.         }
  290.         return res;
  291.     }

  292.     private static class Keys {
  293.         private final byte[] clientKey;
  294.         private final byte[] serverKey;

  295.         public Keys(byte[] clientKey, byte[] serverKey) {
  296.             this.clientKey = clientKey;
  297.             this.serverKey = serverKey;
  298.         }
  299.     }
  300. }