/*
 * Copyright 2004-2007 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package sun.security.jgss.krb5;

import org.ietf.jgss.*;
import sun.security.jgss.*;
import java.security.GeneralSecurityException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import sun.security.krb5.Confounder;
import sun.security.krb5.KrbException;

/**
 * This class represents the new format of GSS tokens, as specified in
 * draft-ietf-krb-wg-gssapi-cfx-07.txt, emitted by the GSSContext.wrap()
 * call. It is a MessageToken except that it also contains plaintext or
 * encrypted data at the end. A WrapToken has certain other rules that are
 * peculiar to it and different from a  MICToken, which is another type of
 * MessageToken. All data in a WrapToken is prepended by a random counfounder
 * of 16 bytes. Thus, all application data is replaced by
 * (confounder || data || tokenHeader || checksum).
 *
 * @author Seema Malkani
 */
class WrapToken_v2 extends MessageToken_v2 {
    /**
     * The size of the random confounder used in a WrapToken.
     */
    static final int CONFOUNDER_SIZE = 16;

    /*
     * A token may come in either in an InputStream or as a
     * byte[]. Store a reference to it in either case and process
     * it's data only later when getData() is called and
     * decryption/copying is needed to be done. Note that JCE can
     * decrypt both from a byte[] and from an InputStream.
     */
    private boolean readTokenFromInputStream = true;
    private InputStream is = null;
    private byte[] tokenBytes = null;
    private int tokenOffset = 0;
    private int tokenLen = 0;

    /*
     * Application data may come from an InputStream or from a
     * byte[]. However, it will always be stored and processed as a
     * byte[] since
     * (a) the MessageDigest class only accepts a byte[] as input and
     * (b) It allows writing to an OuputStream via a CipherOutputStream.
     */
    private byte[] dataBytes = null;
    private int dataOffset = 0;
    private int dataLen = 0;

    // the len of the token data:
    //          (confounder || data || tokenHeader || checksum)
    private int dataSize = 0;

    // Accessed by CipherHelper
    byte[] confounder = null;

    private boolean privacy = false;
    private boolean initiator = true;

    /**
     * Constructs a WrapToken from token bytes obtained from the
     * peer.
     * @param context the mechanism context associated with this
     * token
     * @param tokenBytes the bytes of the token
     * @param tokenOffset the offset of the token
     * @param tokenLen the length of the token
     * @param prop the MessageProp into which characteristics of the
     * parsed token will be stored.
     * @throws GSSException if the token is defective
     */
    public WrapToken_v2(Krb5Context context,
                     byte[] tokenBytes, int tokenOffset, int tokenLen,
                     MessageProp prop)  throws GSSException {

        // Just parse the MessageToken part first
        super(Krb5Token.WRAP_ID_v2, context,
              tokenBytes, tokenOffset, tokenLen, prop);
        this.readTokenFromInputStream = false;

        // rotate token bytes as per RRC
        byte[] new_tokenBytes = new byte[tokenLen];
        if (rotate_left(tokenBytes, tokenOffset, new_tokenBytes, tokenLen)) {
            this.tokenBytes = new_tokenBytes;
            this.tokenOffset = 0;
        } else {
            this.tokenBytes = tokenBytes;
            this.tokenOffset = tokenOffset;
        }

        // Will need the token bytes again when extracting data
        this.tokenLen = tokenLen;
        this.privacy = prop.getPrivacy();

        dataSize = tokenLen - TOKEN_HEADER_SIZE;

        // save initiator
        this.initiator = context.isInitiator();

    }

    /**
     * Constructs a WrapToken from token bytes read on the fly from
     * an InputStream.
     * @param context the mechanism context associated with this
     * token
     * @param is the InputStream containing the token bytes
     * @param prop the MessageProp into which characteristics of the
     * parsed token will be stored.
     * @throws GSSException if the token is defective or if there is
     * a problem reading from the InputStream
     */
    public WrapToken_v2(Krb5Context context,
                     InputStream is, MessageProp prop)
        throws GSSException {

        // Just parse the MessageToken part first
        super(Krb5Token.WRAP_ID_v2, context, is, prop);

        // Will need the token bytes again when extracting data
        this.is = is;
        this.privacy = prop.getPrivacy();

        // get the token length
        try {
            this.tokenLen = is.available();
        } catch (IOException e) {
            throw new GSSException(GSSException.DEFECTIVE_TOKEN, -1,
                                   getTokenName(getTokenId())
                                   + ": " + e.getMessage());
        }

        // data size
        dataSize = tokenLen - TOKEN_HEADER_SIZE;

        // save initiator
        this.initiator = context.isInitiator();
    }

    /**
     * Obtains the application data that was transmitted in this
     * WrapToken.
     * @return a byte array containing the application data
     * @throws GSSException if an error occurs while decrypting any
     * cipher text and checking for validity
     */
    public byte[] getData() throws GSSException {

        byte[] temp = new byte[dataSize];
        int len = getData(temp, 0);
        // len obtained is after removing confounder, tokenHeader and HMAC

        byte[] retVal = new byte[len];
        System.arraycopy(temp, 0, retVal, 0, retVal.length);
        return retVal;
    }

    /**
     * Obtains the application data that was transmitted in this
     * WrapToken, writing it into an application provided output
     * array.
     * @param dataBuf the output buffer into which the data must be
     * written
     * @param dataBufOffset the offset at which to write the data
     * @return the size of the data written
     * @throws GSSException if an error occurs while decrypting any
     * cipher text and checking for validity
     */
    public int getData(byte[] dataBuf, int dataBufOffset)
        throws GSSException {

        if (readTokenFromInputStream)
            getDataFromStream(dataBuf, dataBufOffset);
        else
            getDataFromBuffer(dataBuf, dataBufOffset);

        int retVal = 0;
        if (privacy) {
            retVal = dataSize - confounder.length -
                TOKEN_HEADER_SIZE - cipherHelper.getChecksumLength();
        } else {
            retVal = dataSize - cipherHelper.getChecksumLength();
        }
        return retVal;
    }

    /**
     * Helper routine to obtain the application data transmitted in
     * this WrapToken. It is called if the WrapToken was constructed
     * with a byte array as input.
     * @param dataBuf the output buffer into which the data must be
     * written
     * @param dataBufOffset the offset at which to write the data
     * @throws GSSException if an error occurs while decrypting any
     * cipher text and checking for validity
     */
    private void getDataFromBuffer(byte[] dataBuf, int dataBufOffset)
        throws GSSException {

        int dataPos = tokenOffset + TOKEN_HEADER_SIZE;
        int data_length = 0;

        if (dataPos + dataSize > tokenOffset + tokenLen)
            throw new GSSException(GSSException.DEFECTIVE_TOKEN, -1,
                                   "Insufficient data in "
                                   + getTokenName(getTokenId()));
        // debug("WrapToken cons: data is token is [" +
        //      getHexBytes(tokenBytes, tokenOffset, tokenLen) + "]\n");
        confounder = new byte[CONFOUNDER_SIZE];

        // Do decryption if this token was privacy protected.
        if (privacy) {

            // decrypt data
            cipherHelper.decryptData(this, tokenBytes, dataPos, dataSize,
                                dataBuf, dataBufOffset, getKeyUsage());
            /*
            debug("\t\tDecrypted data is [" +
                getHexBytes(confounder) + " " +
                getHexBytes(dataBuf, dataBufOffset,
                dataSize - CONFOUNDER_SIZE) +
                "]\n");
            */

            data_length = dataSize - CONFOUNDER_SIZE -
                        TOKEN_HEADER_SIZE - cipherHelper.getChecksumLength();
        } else {

            // Token data is in cleartext
            debug("\t\tNo encryption was performed by peer.\n");

            // data
            data_length = dataSize - cipherHelper.getChecksumLength();
            System.arraycopy(tokenBytes, dataPos,
                             dataBuf, dataBufOffset,
                             data_length);
            // debug("\t\tData is: " + getHexBytes(dataBuf, data_length));

            /*
             * Make sure checksum is not corrupt
             */
            if (!verifySign(dataBuf, dataBufOffset, data_length)) {
                throw new GSSException(GSSException.BAD_MIC, -1,
                         "Corrupt checksum in Wrap token");
            }
        }
    }

    /**
     * Helper routine to obtain the application data transmitted in
     * this WrapToken. It is called if the WrapToken was constructed
     * with an Inputstream.
     * @param dataBuf the output buffer into which the data must be
     * written
     * @param dataBufOffset the offset at which to write the data
     * @throws GSSException if an error occurs while decrypting any
     * cipher text and checking for validity
     */
    private void getDataFromStream(byte[] dataBuf, int dataBufOffset)
        throws GSSException {

        int data_length = 0;
        // Don't check the token length. Data will be read on demand from
        // the InputStream.
        // debug("WrapToken cons: data will be read from InputStream.\n");

        confounder = new byte[CONFOUNDER_SIZE];

        try {
            // Do decryption if this token was privacy protected.
            if (privacy) {

                cipherHelper.decryptData(this, is, dataSize,
                    dataBuf, dataBufOffset, getKeyUsage());

                /*
                debug("\t\tDecrypted data is [" +
                     getHexBytes(confounder) + " " +
                     getHexBytes(dataBuf, dataBufOffset,
                  dataSize - CONFOUNDER_SIZE) +
                     "]\n");
                */
                data_length = dataSize - CONFOUNDER_SIZE -
                        TOKEN_HEADER_SIZE - cipherHelper.getChecksumLength();
            } else {

                // Token data is in cleartext
                debug("\t\tNo encryption was performed by peer.\n");
                readFully(is, confounder);

                // read the data
                data_length = dataSize - cipherHelper.getChecksumLength();
                readFully(is, dataBuf, dataBufOffset, data_length);

                /*
                 * Make sure checksum is not corrupt
                 */
                if (!verifySign(dataBuf, dataBufOffset, data_length)) {
                    throw new GSSException(GSSException.BAD_MIC, -1,
                         "Corrupt checksum in Wrap token");
                }
            }
        } catch (IOException e) {
            throw new GSSException(GSSException.DEFECTIVE_TOKEN, -1,
                                   getTokenName(getTokenId())
                                   + ": " + e.getMessage());
        }

    }


    public WrapToken_v2(Krb5Context context, MessageProp prop,
                     byte[] dataBytes, int dataOffset, int dataLen)
        throws GSSException {

        super(Krb5Token.WRAP_ID_v2, context);

        confounder = Confounder.bytes(CONFOUNDER_SIZE);

        dataSize = confounder.length + dataLen + TOKEN_HEADER_SIZE +
                        cipherHelper.getChecksumLength();
        this.dataBytes = dataBytes;
        this.dataOffset = dataOffset;
        this.dataLen = dataLen;

        // save initiator
        this.initiator = context.isInitiator();

        // debug("\nWrapToken cons: data to wrap is [" +
        // getHexBytes(confounder) + " " +
        // getHexBytes(dataBytes, dataOffset, dataLen) + "]\n");

        genSignAndSeqNumber(prop,
                            dataBytes, dataOffset, dataLen);

        /*
         * If the application decides to ask for privacy when the context
         * did not negotiate for it, do not provide it. The peer might not
         * have support for it. The app will realize this with a call to
         * pop.getPrivacy() after wrap().
         */
        if (!context.getConfState())
            prop.setPrivacy(false);

        privacy = prop.getPrivacy();
    }

    public void encode(OutputStream os) throws IOException, GSSException {

        super.encode(os);

        // debug("\n\nWriting data: [");
        if (!privacy) {

            // Wrap Tokens (without confidentiality) =
            // { 16 byte token_header | plaintext | 12-byte HMAC }
            // where HMAC is on { plaintext | token_header }

            // calculate checksum
            byte[] checksum = getChecksum(dataBytes, dataOffset, dataLen);

            // data
            // debug(" " + getHexBytes(dataBytes, dataOffset, dataLen));
            os.write(dataBytes, dataOffset, dataLen);

            // write HMAC
            // debug(" " + getHexBytes(checksum,
            //                  cipherHelper.getChecksumLength()));
            os.write(checksum);

        } else {

            // Wrap Tokens (with confidentiality) =
            // { 16 byte token_header |
            // Encrypt(16-byte confounder | plaintext | token_header) |
            // 12-byte HMAC }

            cipherHelper.encryptData(this, confounder, getTokenHeader(),
                dataBytes, dataOffset, dataLen, getKeyUsage(), os);

        }
        // debug("]\n");
    }

    public byte[] encode() throws IOException, GSSException {
        // XXX Fine tune this initial size
        ByteArrayOutputStream bos = new ByteArrayOutputStream(dataSize + 50);
        encode(bos);
        return bos.toByteArray();
    }

    public int encode(byte[] outToken, int offset)
        throws IOException, GSSException  {

        int retVal = 0;

        // Token header is small
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        super.encode(bos);
        byte[] header = bos.toByteArray();
        System.arraycopy(header, 0, outToken, offset, header.length);
        offset += header.length;

        // debug("WrapToken.encode: Writing data: [");
        if (!privacy) {

            // Wrap Tokens (without confidentiality) =
            // { 16 byte token_header | plaintext | 12-byte HMAC }
            // where HMAC is on { plaintext | token_header }

            // calculate checksum
            byte[] checksum = getChecksum(dataBytes, dataOffset, dataLen);

            // data
            // debug(" " + getHexBytes(dataBytes, dataOffset, dataLen));
            System.arraycopy(dataBytes, dataOffset, outToken, offset,
                             dataLen);
            offset += dataLen;

            // write HMAC
            // debug(" " + getHexBytes(checksum,
            //                  cipherHelper.getChecksumLength()));
            System.arraycopy(checksum, 0, outToken, offset,
                                cipherHelper.getChecksumLength());

            retVal = header.length + dataLen + cipherHelper.getChecksumLength();
        } else {

            // Wrap Tokens (with confidentiality) =
            // { 16 byte token_header |
            // Encrypt(16-byte confounder | plaintext | token_header) |
            // 12-byte HMAC }
            int cLen = cipherHelper.encryptData(this, confounder,
                        getTokenHeader(), dataBytes, dataOffset, dataLen,
                        outToken, offset, getKeyUsage());

            retVal = header.length + cLen;
            // debug(getHexBytes(outToken, offset, dataSize));
        }

        // debug("]\n");

        // %%% assume that plaintext length == ciphertext len
        return retVal;

    }

    protected int getKrb5TokenSize() throws GSSException {
        return (getTokenSize() + dataSize);
    }

    // This implementation is way to conservative. And it certainly
    // doesn't return the maximum limit.
    static int getSizeLimit(int qop, boolean confReq, int maxTokenSize,
        CipherHelper ch) throws GSSException {
        return (GSSHeader.getMaxMechTokenSize(OID, maxTokenSize) -
                (getTokenSize(ch) + CONFOUNDER_SIZE) - 8 /* safety */);
    }
}
