/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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.conscrypt;

import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.RSAKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.Arrays;
import java.util.Vector;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;

/**
 * Base class for ClientHandshakeImpl and ServerHandshakeImpl classes.
 * @see <a href="http://www.ietf.org/rfc/rfc2246.txt">TLS 1.0 spec., 7.4.
 * Handshake protocol</a>
 *
 */
public abstract class HandshakeProtocol {

    /**
     * Handshake status NEED_UNWRAP - HandshakeProtocol needs to receive data
     */
    public static final int NEED_UNWRAP = 1;

    /**
     * Handshake status NOT_HANDSHAKING - is not currently handshaking
     */
    public static final int NOT_HANDSHAKING = 2;

    /**
     * Handshake status FINISHED - HandshakeProtocol has just finished
     */
    public static final int FINISHED = 3;

    /**
     * Handshake status NEED_TASK - HandshakeProtocol needs the results of delegated task
     */
    public static final int NEED_TASK = 4;

    /**
     * Current handshake status
     */
    protected int status = NOT_HANDSHAKING;

    /**
     * IO stream for income/outcome handshake data
     */
    protected HandshakeIODataStream io_stream = new HandshakeIODataStream();

    /**
     * SSL Record Protocol implementation.
     */
    protected SSLRecordProtocol recordProtocol;

    /**
     * SSLParametersImpl suplied by SSLSocket or SSLEngine
     */
    protected SSLParametersImpl parameters;

    /**
     * Delegated tasks for this handshake implementation
     */
    protected Vector<DelegatedTask> delegatedTasks = new Vector<DelegatedTask>();

    /**
     * Indicates non-blocking handshake
     */
    protected boolean nonBlocking;

    /**
     * Pending session
     */
    protected SSLSessionImpl session;

    /**
     * Sent and received handshake messages
     */
    protected ClientHello clientHello;
    protected ServerHello serverHello;
    protected CertificateMessage serverCert;
    protected ServerKeyExchange serverKeyExchange;
    protected CertificateRequest certificateRequest;
    protected ServerHelloDone serverHelloDone;
    protected CertificateMessage clientCert;
    protected ClientKeyExchange clientKeyExchange;
    protected CertificateVerify certificateVerify;
    protected Finished clientFinished;
    protected Finished serverFinished;

    /**
     * Indicates that change cipher spec message has been received
     */
    protected boolean changeCipherSpecReceived = false;

    /**
     * Indicates previous session resuming
     */
    protected boolean isResuming = false;

    /**
     *  Premaster secret
     */
    protected byte[] preMasterSecret;

    /**
     * Exception occured in delegated task
     */
    protected Exception delegatedTaskErr;

    // reference verify_data used to verify finished message
    private byte[] verify_data = new byte[12];

    // Encoding of "master secret" string: "master secret".getBytes()
    private byte[] master_secret_bytes =
            {109, 97, 115, 116, 101, 114, 32, 115, 101, 99, 114, 101, 116 };

    // indicates whether protocol needs to send change cipher spec message
    private boolean needSendCCSpec = false;

    // indicates whether protocol needs to send change cipher spec message
    protected boolean needSendHelloRequest = false;

    /**
     * SSLEngine owning this HandshakeProtocol
     */
    public SSLEngineImpl engineOwner;

    /**
     * SSLSocket owning this HandshakeProtocol
     */
    public SSLSocketImpl socketOwner;

    /**
     * Creates HandshakeProtocol instance
     * @param owner
     */
    protected HandshakeProtocol(Object owner) {
        if (owner instanceof SSLEngineImpl) {
            engineOwner = (SSLEngineImpl) owner;
            nonBlocking = true;
            this.parameters = engineOwner.sslParameters;
        }
        else if (owner instanceof SSLSocketImpl) {
            socketOwner = (SSLSocketImpl) owner;
            nonBlocking = false;
            this.parameters = socketOwner.sslParameters;
        }
    }

    /**
     * Sets SSL Record Protocol
     * @param recordProtocol
     */
    public void setRecordProtocol(SSLRecordProtocol recordProtocol) {
        this.recordProtocol = recordProtocol;
    }

    /**
     * Start session negotiation
     */
    public abstract void start();

    /**
     * Stops the current session renegotiation process.
     * Such functionality is needed when it is session renegotiation
     * process and no_renegotiation alert message is received
     * from another peer.
     * @param session
     */
    protected void stop() {
        clearMessages();
        status = NOT_HANDSHAKING;
    }

    /**
     * Returns handshake status
     */
    public SSLEngineResult.HandshakeStatus getStatus() {
        if (io_stream.hasData() || needSendCCSpec ||
                needSendHelloRequest || delegatedTaskErr != null) {
            return SSLEngineResult.HandshakeStatus.NEED_WRAP;
        }
        if (!delegatedTasks.isEmpty()) {
            return SSLEngineResult.HandshakeStatus.NEED_TASK;
        }

        switch (status) {
        case HandshakeProtocol.NEED_UNWRAP:
            return SSLEngineResult.HandshakeStatus.NEED_UNWRAP;
        case HandshakeProtocol.FINISHED:
            status = NOT_HANDSHAKING;
            clearMessages();
            return SSLEngineResult.HandshakeStatus.FINISHED;
        default: // HandshakeProtocol.NOT_HANDSHAKING:
            return SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING;
        }
    }

    /**
     * Returns pending session
     * @return session
     */
    public SSLSessionImpl getSession() {
        return session;
    }

    protected void sendChangeCipherSpec() {
        needSendCCSpec = true;
    }

    protected void sendHelloRequest() {
        needSendHelloRequest = true;
    }

    /**
     * Proceses inbound ChangeCipherSpec message
     */
    abstract void receiveChangeCipherSpec();

    /**
     * Creates and sends finished message
     */
    abstract void makeFinished();

    /**
     * Proceses inbound handshake messages
     * @param bytes
     */
    public abstract void unwrap(byte[] bytes);

    /**
     * Processes SSLv2 Hello message
     * @param bytes
     */
    public abstract void unwrapSSLv2(byte[] bytes);

    /**
     * Processes outbound handshake messages
     */
    public byte[] wrap() {
        if (delegatedTaskErr != null) {
            // process error occured in delegated task
            Exception e = delegatedTaskErr;
            delegatedTaskErr = null;
            fatalAlert(AlertProtocol.HANDSHAKE_FAILURE,
                    "Error occured in delegated task:" + e.getMessage(), e);
        }
        if (io_stream.hasData()) {
            return recordProtocol.wrap(ContentType.HANDSHAKE, io_stream);
        } else if (needSendCCSpec) {
            makeFinished();
            needSendCCSpec = false;
            return recordProtocol.getChangeCipherSpecMesage(getSession());
        } else if (needSendHelloRequest) {
            needSendHelloRequest = false;
            return recordProtocol.wrap(ContentType.HANDSHAKE,
                    // hello request message
                    // (see TLS v 1 specification:
                    // http://www.ietf.org/rfc/rfc2246.txt)
                    new byte[] {0, 0, 0, 0}, 0, 4);
        } else {
            return null; // nothing to send;
        }
    }

    /**
     * Sends fatal alert, breaks execution
     *
     * @param description
     */
    protected void sendWarningAlert(byte description) {
        recordProtocol.alert(AlertProtocol.WARNING, description);
    }

    /**
     * Sends fatal alert, breaks execution
     *
     * @param description
     * @param reason
     */
    protected void fatalAlert(byte description, String reason) {
        throw new AlertException(description, new SSLHandshakeException(reason));
    }

    /**
     * Sends fatal alert, breaks execution
     *
     * @param description
     * @param reason
     * @param cause
     */
    protected void fatalAlert(byte description, String reason, Exception cause) {
        throw new AlertException(description, new SSLException(reason, cause));
    }

    /**
     * Sends fatal alert, breaks execution
     *
     * @param description
     * @param cause
     */
    protected void fatalAlert(byte description, SSLException cause) {
        throw new AlertException(description, cause);
    }

    /**
     * Computers reference TLS verify_data that is used to verify finished message
     * @see <a href="http://www.ietf.org/rfc/rfc2246.txt">TLS spec. 7.4.9. Finished</a>
     * @param label
     */
    protected void computerReferenceVerifyDataTLS(String label) {
        computerVerifyDataTLS(label, verify_data);
    }

    /**
     * Computer TLS verify_data
     * @see <a href="http://www.ietf.org/rfc/rfc2246.txt">TLS spec. 7.4.9. Finished</a>
     * @param label
     * @param buf
     */
    protected void computerVerifyDataTLS(String label, byte[] buf) {
        byte[] md5_digest = io_stream.getDigestMD5();
        byte[] sha_digest = io_stream.getDigestSHA();

        byte[] digest = new byte[md5_digest.length + sha_digest.length];
        System.arraycopy(md5_digest, 0, digest, 0, md5_digest.length);
        System.arraycopy(sha_digest, 0, digest, md5_digest.length,
                sha_digest.length);
        try {
            PRF.computePRF(buf, session.master_secret,
                    label.getBytes(), digest);
        } catch (GeneralSecurityException e) {
            fatalAlert(AlertProtocol.INTERNAL_ERROR, "PRF error", e);
        }
    }

    /**
     * Computer reference SSLv3 verify_data that is used to verify finished message
     * @see "SSLv3 spec. 7.6.9. Finished"
     * @param label
     */
    protected void computerReferenceVerifyDataSSLv3(byte[] sender) {
        verify_data = new byte[36];
        computerVerifyDataSSLv3(sender, verify_data);
    }

    /**
     * Computer SSLv3 verify_data
     * @see "SSLv3 spec. 7.6.9. Finished"
     * @param label
     * @param buf
     */
    protected void computerVerifyDataSSLv3(byte[] sender, byte[] buf) {
        MessageDigest md5;
        MessageDigest sha;
        try {
            md5 = MessageDigest.getInstance("MD5");
            sha = MessageDigest.getInstance("SHA-1");
        } catch (Exception e) {
            fatalAlert(AlertProtocol.INTERNAL_ERROR,
                       "Could not initialize the Digest Algorithms.",
                       e);
            return;
        }
        try {
            byte[] handshake_messages = io_stream.getMessages();
            md5.update(handshake_messages);
            md5.update(sender);
            md5.update(session.master_secret);
            byte[] b = md5.digest(SSLv3Constants.MD5pad1);
            md5.update(session.master_secret);
            md5.update(SSLv3Constants.MD5pad2);
            System.arraycopy(md5.digest(b), 0, buf, 0, 16);

            sha.update(handshake_messages);
            sha.update(sender);
            sha.update(session.master_secret);
            b = sha.digest(SSLv3Constants.SHApad1);
            sha.update(session.master_secret);
            sha.update(SSLv3Constants.SHApad2);
            System.arraycopy(sha.digest(b), 0, buf, 16, 20);
        } catch (Exception e) {
            fatalAlert(AlertProtocol.INTERNAL_ERROR, "INTERNAL ERROR", e);

        }
    }

    /**
     * Verifies finished data
     *
     * @param data
     * @param isServer
     */
    protected void verifyFinished(byte[] data) {
        if (!Arrays.equals(verify_data, data)) {
            fatalAlert(AlertProtocol.HANDSHAKE_FAILURE, "Incorrect FINISED");
        }
    }

    /**
     * Sends fatal alert "UNEXPECTED MESSAGE"
     *
     */
    protected void unexpectedMessage() {
        fatalAlert(AlertProtocol.UNEXPECTED_MESSAGE, "UNEXPECTED MESSAGE");
    }

    /**
     * Writes message to HandshakeIODataStream
     *
     * @param message
     */
    public void send(Message message) {
        io_stream.writeUint8(message.getType());
        io_stream.writeUint24(message.length());
        message.send(io_stream);
    }

    /**
     * Computers master secret
     *
     */
    public void computerMasterSecret() {
        byte[] seed = new byte[64];
        System.arraycopy(clientHello.getRandom(), 0, seed, 0, 32);
        System.arraycopy(serverHello.getRandom(), 0, seed, 32, 32);
        session.master_secret = new byte[48];
        if (serverHello.server_version[1] == 1) { // TLSv1
            try {
                PRF.computePRF(session.master_secret, preMasterSecret,
                        master_secret_bytes, seed);
            } catch (GeneralSecurityException e) {
                fatalAlert(AlertProtocol.INTERNAL_ERROR, "PRF error", e);
            }
        } else { // SSL3.0
            PRF.computePRF_SSLv3(session.master_secret, preMasterSecret, seed);
        }

        //delete preMasterSecret from memory
        Arrays.fill(preMasterSecret, (byte)0);
        preMasterSecret = null;
    }

    /**
     * Returns a delegated task.
     * @return Delegated task or null
     */
    public Runnable getTask() {
        if (delegatedTasks.isEmpty()) {
            return null;
        }
        return delegatedTasks.remove(0);
    }

    /**
     * Clears previously sent and received handshake messages
     */
    protected void clearMessages() {
        io_stream.clearBuffer();
        clientHello = null;
        serverHello = null;
        serverCert = null;
        serverKeyExchange = null;
        certificateRequest = null;
        serverHelloDone = null;
        clientCert = null;
        clientKeyExchange = null;
        certificateVerify = null;
        clientFinished = null;
        serverFinished = null;
    }

    /**
     * Returns RSA key length
     * @param pk
     * @return
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    protected static int getRSAKeyLength(PublicKey pk)
            throws NoSuchAlgorithmException, InvalidKeySpecException {

        BigInteger mod;
        if (pk instanceof RSAKey) {
            mod = ((RSAKey) pk).getModulus();
        } else {
            KeyFactory kf = KeyFactory.getInstance("RSA");
            mod = kf.getKeySpec(pk, RSAPublicKeySpec.class)
                    .getModulus();
        }
        return mod.bitLength();
    }

    /**
     * Shuts down the protocol. It will be impossible to use the instance
     * after calling this method.
     */
    protected void shutdown() {
        clearMessages();
        session = null;
        preMasterSecret = null;
        delegatedTasks.clear();
    }
}
