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<String, SCRAMSHA1Mechanism.Keys> CACHE = new LruCache(10);
   private SCRAMSHA1Mechanism.State state;
   private String clientRandomAscii;
   private String clientFirstMessageBare;
   private byte[] serverSignature;

   public SCRAMSHA1Mechanism() {
      this.state = SCRAMSHA1Mechanism.State.INITIAL;
   }

   protected void authenticateInternal(CallbackHandler cbh) throws SmackException {
      throw new UnsupportedOperationException("CallbackHandler not (yet) supported");
   }

   protected byte[] getAuthenticationText() throws SmackException {
      this.clientRandomAscii = this.getRandomAscii();
      String saslPrepedAuthcId = saslPrep(this.authenticationId);
      this.clientFirstMessageBare = "n=" + escape(saslPrepedAuthcId) + ",r=" + this.clientRandomAscii;
      String clientFirstMessage = "n,," + this.clientFirstMessageBare;
      this.state = SCRAMSHA1Mechanism.State.AUTH_TEXT_SENT;
      return toBytes(clientFirstMessage);
   }

   public String getName() {
      return "SCRAM-SHA-1";
   }

   public int getPriority() {
      return 110;
   }

   public SCRAMSHA1Mechanism newInstance() {
      return new SCRAMSHA1Mechanism();
   }

   public void checkIfSuccessfulOrThrow() throws SmackException {
      if (this.state != SCRAMSHA1Mechanism.State.VALID_SERVER_RESPONSE) {
         throw new SmackException("SCRAM-SHA1 is missing valid server response");
      }
   }

   protected byte[] evaluateChallenge(byte[] challenge) throws SmackException {
      String challengeString = new String(challenge);
      switch(this.state) {
      case AUTH_TEXT_SENT:
         Map<Character, String> attributes = parseAttributes(challengeString);
         String rvalue = (String)attributes.get('r');
         if (rvalue == null) {
            throw new SmackException("Server random ASCII is null");
         } else if (rvalue.length() <= this.clientRandomAscii.length()) {
            throw new SmackException("Server random ASCII is shorter then client random ASCII");
         } else {
            String receivedClientRandomAscii = rvalue.substring(0, this.clientRandomAscii.length());
            if (!receivedClientRandomAscii.equals(this.clientRandomAscii)) {
               throw new SmackException("Received client random ASCII does not match client random ASCII");
            } else {
               String iterationsString = (String)attributes.get('i');
               if (iterationsString == null) {
                  throw new SmackException("Iterations attribute not set");
               } else {
                  int iterations;
                  try {
                     iterations = Integer.parseInt(iterationsString);
                  } catch (NumberFormatException var21) {
                     throw new SmackException("Exception parsing iterations", var21);
                  }

                  String salt = (String)attributes.get('s');
                  if (salt == null) {
                     throw new SmackException("SALT not send");
                  }

                  String clientFinalMessageWithoutProof = "c=" + Base64.encode("n,,") + ",r=" + rvalue;
                  byte[] authMessage = toBytes(this.clientFirstMessageBare + ',' + challengeString + ',' + clientFinalMessageWithoutProof);
                  String cacheKey = this.password + ',' + salt;
                  SCRAMSHA1Mechanism.Keys keys = (SCRAMSHA1Mechanism.Keys)CACHE.get(cacheKey);
                  byte[] serverKey;
                  byte[] clientKey;
                  byte[] storedKey;
                  if (keys == null) {
                     storedKey = hi(saslPrep(this.password), Base64.decode(salt), iterations);
                     serverKey = hmac(storedKey, SERVER_KEY_BYTES);
                     clientKey = hmac(storedKey, CLIENT_KEY_BYTES);
                     keys = new SCRAMSHA1Mechanism.Keys(clientKey, serverKey);
                     CACHE.put(cacheKey, keys);
                  } else {
                     serverKey = keys.serverKey;
                     clientKey = keys.clientKey;
                  }

                  this.serverSignature = hmac(serverKey, authMessage);
                  storedKey = SHA1.bytes(clientKey);
                  byte[] clientSignature = hmac(storedKey, authMessage);
                  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);
                  this.state = SCRAMSHA1Mechanism.State.RESPONSE_SENT;
                  return toBytes(clientFinalMessage);
               }
            }
         }
      case RESPONSE_SENT:
         String clientCalculatedServerFinalMessage = "v=" + Base64.encodeToString(this.serverSignature);
         if (!clientCalculatedServerFinalMessage.equals(challengeString)) {
            throw new SmackException("Server final message does not match calculated one");
         }

         this.state = SCRAMSHA1Mechanism.State.VALID_SERVER_RESPONSE;
         return null;
      default:
         throw new SmackException("Invalid state");
      }
   }

   private static Map<Character, String> parseAttributes(String string) throws SmackException {
      if (string.length() == 0) {
         return Collections.emptyMap();
      } else {
         String[] keyValuePairs = string.split(",");
         Map<Character, String> res = new HashMap(keyValuePairs.length, 1.0F);
         String[] arr$ = keyValuePairs;
         int len$ = keyValuePairs.length;

         for(int i$ = 0; i$ < len$; ++i$) {
            String keyValuePair = arr$[i$];
            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;
      }
   }

   String getRandomAscii() {
      int count = 0;
      char[] randomAscii = new char[32];

      while(count < 32) {
         int r = RANDOM.nextInt(128);
         char c = (char)r;
         if (isPrintableNonCommaAsciiChar(c)) {
            randomAscii[count++] = c;
         }
      }

      return new String(randomAscii);
   }

   private static boolean isPrintableNonCommaAsciiChar(char c) {
      if (c == ',') {
         return false;
      } else {
         return c >= ' ' && c < 127;
      }
   }

   private static String escape(String string) {
      StringBuilder sb = new StringBuilder((int)((double)string.length() * 1.1D));

      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);
         }
      }

      return sb.toString();
   }

   private static byte[] hmac(byte[] key, byte[] str) throws SmackException {
      try {
         return MAC.hmacsha1(key, str);
      } catch (InvalidKeyException var3) {
         throw new SmackException("SCRAM-SHA-1 HMAC-SHA1 Exception", var3);
      }
   }

   private static byte[] hi(String str, byte[] salt, int iterations) throws SmackException {
      byte[] key = str.getBytes();
      byte[] u = hmac(key, ByteUtils.concact(salt, ONE));
      byte[] res = (byte[])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;
      }
   }

   private static enum State {
      INITIAL,
      AUTH_TEXT_SENT,
      RESPONSE_SENT,
      VALID_SERVER_RESPONSE;
   }
}
