/*
 * Decompiled with CFR 0.152.
 */
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 = SCRAMSHA1Mechanism.toBytes("Client Key");
    private static final byte[] SERVER_KEY_BYTES = SCRAMSHA1Mechanism.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, Keys> CACHE = new LruCache<String, Keys>(10);
    private State state = State.INITIAL;
    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 {
        this.clientRandomAscii = this.getRandomAscii();
        String saslPrepedAuthcId = SCRAMSHA1Mechanism.saslPrep(this.authenticationId);
        this.clientFirstMessageBare = "n=" + SCRAMSHA1Mechanism.escape(saslPrepedAuthcId) + ",r=" + this.clientRandomAscii;
        String clientFirstMessage = DEFAULT_GS2_HEADER + this.clientFirstMessageBare;
        this.state = State.AUTH_TEXT_SENT;
        return SCRAMSHA1Mechanism.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 (this.state != State.VALID_SERVER_RESPONSE) {
            throw new SmackException("SCRAM-SHA1 is missing valid server response");
        }
    }

    @Override
    protected byte[] evaluateChallenge(byte[] challenge) throws SmackException {
        String challengeString = new String(challenge);
        switch (this.state) {
            case AUTH_TEXT_SENT: {
                byte[] clientKey;
                byte[] serverKey;
                int iterations;
                String serverFirstMessage = challengeString;
                Map<Character, String> attributes = SCRAMSHA1Mechanism.parseAttributes(challengeString);
                String rvalue = attributes.get(Character.valueOf('r'));
                if (rvalue == null) {
                    throw new SmackException("Server random ASCII is null");
                }
                if (rvalue.length() <= this.clientRandomAscii.length()) {
                    throw new SmackException("Server random ASCII is shorter then client random ASCII");
                }
                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");
                }
                String iterationsString = attributes.get(Character.valueOf('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);
                }
                String salt = attributes.get(Character.valueOf('s'));
                if (salt == null) {
                    throw new SmackException("SALT not send");
                }
                String clientFinalMessageWithoutProof = "c=" + Base64.encode(DEFAULT_GS2_HEADER) + ",r=" + rvalue;
                byte[] authMessage = SCRAMSHA1Mechanism.toBytes(this.clientFirstMessageBare + ',' + serverFirstMessage + ',' + clientFinalMessageWithoutProof);
                String cacheKey = this.password + ',' + salt;
                Keys keys = CACHE.get(cacheKey);
                if (keys == null) {
                    byte[] saltedPassword = SCRAMSHA1Mechanism.hi(SCRAMSHA1Mechanism.saslPrep(this.password), Base64.decode(salt), iterations);
                    serverKey = SCRAMSHA1Mechanism.hmac(saltedPassword, SERVER_KEY_BYTES);
                    clientKey = SCRAMSHA1Mechanism.hmac(saltedPassword, CLIENT_KEY_BYTES);
                    keys = new Keys(clientKey, serverKey);
                    CACHE.put(cacheKey, keys);
                } else {
                    serverKey = keys.serverKey;
                    clientKey = keys.clientKey;
                }
                this.serverSignature = SCRAMSHA1Mechanism.hmac(serverKey, authMessage);
                byte[] storedKey = SHA1.bytes(clientKey);
                byte[] clientSignature = SCRAMSHA1Mechanism.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 = State.RESPONSE_SENT;
                return SCRAMSHA1Mechanism.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 = State.VALID_SERVER_RESPONSE;
                break;
            }
            default: {
                throw new SmackException("Invalid state");
            }
        }
        return null;
    }

    private static Map<Character, String> parseAttributes(String string) throws SmackException {
        if (string.length() == 0) {
            return Collections.emptyMap();
        }
        String[] keyValuePairs = string.split(",");
        HashMap<Character, String> res = new HashMap<Character, String>(keyValuePairs.length, 1.0f);
        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(Character.valueOf(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 (!SCRAMSHA1Mechanism.isPrintableNonCommaAsciiChar(c)) continue;
            randomAscii[count++] = c;
        }
        return new String(randomAscii);
    }

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

    private static String escape(String string) {
        StringBuilder sb = new StringBuilder((int)((double)string.length() * 1.1));
        block4: for (int i = 0; i < string.length(); ++i) {
            char c = string.charAt(i);
            switch (c) {
                case ',': {
                    sb.append("=2C");
                    continue block4;
                }
                case '=': {
                    sb.append("=3D");
                    continue block4;
                }
                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 e) {
            throw new SmackException("SCRAM-SHA-1 HMAC-SHA1 Exception", e);
        }
    }

    private static byte[] hi(String str, byte[] salt, int iterations) throws SmackException {
        byte[] key = str.getBytes();
        byte[] u = SCRAMSHA1Mechanism.hmac(key, ByteUtils.concact(salt, ONE));
        byte[] res = (byte[])u.clone();
        for (int i = 1; i < iterations; ++i) {
            u = SCRAMSHA1Mechanism.hmac(key, u);
            for (int j = 0; j < u.length; ++j) {
                int n = j;
                res[n] = (byte)(res[n] ^ 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;

    }
}
