jdk/src/share/classes/sun/security/jgss/krb5/MessageToken.java
author jjg
Mon, 15 Aug 2011 11:48:20 -0700
changeset 10336 0bb1999251f8
parent 5506 202f599c92aa
child 21278 ef8a3a2a72f2
permissions -rw-r--r--
7064075: Security libraries don't build with javac -Xlint:all,-deprecation -Werror Reviewed-by: xuelei, mullan Contributed-by: alexandre.boulgakov@oracle.com

/*
 * Copyright (c) 2000, 2011, Oracle and/or its affiliates. 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.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.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.security.MessageDigest;

/**
 * This class is a base class for other token definitions that pertain to
 * per-message GSS-API calls. Conceptually GSS-API has two types of
 * per-message tokens: WrapToken and MicToken. They differ in the respect
 * that a WrapToken carries additional plaintext or ciphertext application
 * data besides just the sequence number and checksum. This class
 * encapsulates the commonality in the structure of the WrapToken and the
 * MicToken. This structure can be represented as:
 * <p>
 * <pre>
 *     0..1           TOK_ID          Identification field.
 *                                    01 01 - Mic token
 *                                    02 01 - Wrap token
 *     2..3           SGN_ALG         Checksum algorithm indicator.
 *                                    00 00 - DES MAC MD5
 *                                    01 00 - MD2.5
 *                                    02 00 - DES MAC
 *                                    04 00 - HMAC SHA1 DES3-KD
 *                                    11 00 - RC4-HMAC
 *     4..5           SEAL_ALG        ff ff - none
 *                                    00 00 - DES
 *                                    02 00 - DES3-KD
 *                                    10 00 - RC4-HMAC
 *     6..7           Filler          Contains ff ff
 *     8..15          SND_SEQ         Encrypted sequence number field.
 *     16..s+15       SGN_CKSUM       Checksum of plaintext padded data,
 *                                   calculated according to algorithm
 *                                  specified in SGN_ALG field.
 *     s+16..last     Data            encrypted or plaintext padded data
 * </pre>
 * Where "s" indicates the size of the checksum.
 * <p>
 * As always, this is preceeded by a GSSHeader.
 *
 * @author Mayank Upadhyay
 * @author Ram Marti
 * @see sun.security.jgss.GSSHeader
 */

abstract class MessageToken extends Krb5Token {
    /* Fields in header minus checksum size */
    private static final int TOKEN_NO_CKSUM_SIZE = 16;

    /**
     * Filler data as defined in the specification of the Kerberos v5 GSS-API
     * Mechanism.
     */
    private static final int FILLER = 0xffff;

     // Signing algorithm values (for the SNG_ALG field)

     // From RFC 1964
     /* Use a DES MAC MD5 checksum */
    static final int SGN_ALG_DES_MAC_MD5 = 0x0000;

     /* Use DES MAC checksum. */
    static final int SGN_ALG_DES_MAC     = 0x0200;

     // From draft-raeburn-cat-gssapi-krb5-3des-00
     /* Use a HMAC SHA1 DES3 -KD checksum */
    static final int SGN_ALG_HMAC_SHA1_DES3_KD = 0x0400;

     // Sealing algorithm values (for the SEAL_ALG field)

     // RFC 1964
    /**
     * A value for the SEAL_ALG field that indicates that no encryption was
     * used.
     */
    static final int SEAL_ALG_NONE    = 0xffff;
     /* Use DES CBC encryption algorithm. */
    static final int SEAL_ALG_DES = 0x0000;

    // From draft-raeburn-cat-gssapi-krb5-3des-00
    /**
     * Use DES3-KD sealing algorithm. (draft-raeburn-cat-gssapi-krb5-3des-00)
     * This algorithm uses triple-DES with key derivation, with a usage
     * value KG_USAGE_SEAL.  Padding is still to 8-byte multiples, and the
     * IV for encrypting application data is zero.
     */
    static final int SEAL_ALG_DES3_KD = 0x0200;

    // draft draft-brezak-win2k-krb-rc4-hmac-04.txt
    static final int SEAL_ALG_ARCFOUR_HMAC = 0x1000;
    static final int SGN_ALG_HMAC_MD5_ARCFOUR = 0x1100;

    private static final int TOKEN_ID_POS = 0;
    private static final int SIGN_ALG_POS = 2;
    private static final int SEAL_ALG_POS = 4;

    private int seqNumber;

    private boolean confState = true;
    private boolean initiator = true;

    private int tokenId = 0;
    private GSSHeader gssHeader = null;
    private MessageTokenHeader tokenHeader = null;
    private byte[] checksum = null;
    private byte[] encSeqNumber = null;
    private byte[] seqNumberData = null;

    /* cipher instance used by the corresponding GSSContext */
    CipherHelper cipherHelper = null;


    /**
     * Constructs a MessageToken from a byte array. If there are more bytes
     * in the array than needed, the extra bytes are simply ignroed.
     *
     * @param tokenId the token id that should be contained in this token as
     * it is read.
     * @param context the Kerberos context associated with this token
     * @param tokenBytes the byte array containing the token
     * @param tokenOffset the offset where the token begins
     * @param tokenLen the length of the token
     * @param prop the MessageProp structure in which the properties of the
     * token should be stored.
     * @throws GSSException if there is a problem parsing the token
     */
    MessageToken(int tokenId, Krb5Context context,
                 byte[] tokenBytes, int tokenOffset, int tokenLen,
                 MessageProp prop) throws GSSException {
        this(tokenId, context,
             new ByteArrayInputStream(tokenBytes, tokenOffset, tokenLen),
             prop);
    }

    /**
     * Constructs a MessageToken from an InputStream. Bytes will be read on
     * demand and the thread might block if there are not enough bytes to
     * complete the token.
     *
     * @param tokenId the token id that should be contained in this token as
     * it is read.
     * @param context the Kerberos context associated with this token
     * @param is the InputStream from which to read
     * @param prop the MessageProp structure in which the properties of the
     * token should be stored.
     * @throws GSSException if there is a problem reading from the
     * InputStream or parsing the token
     */
    MessageToken(int tokenId, Krb5Context context, InputStream is,
                 MessageProp prop) throws GSSException {
        init(tokenId, context);

        try {
            gssHeader = new GSSHeader(is);

            if (!gssHeader.getOid().equals((Object)OID)) {
                throw new GSSException(GSSException.DEFECTIVE_TOKEN, -1,
                                       getTokenName(tokenId));
            }
            if (!confState) {
                prop.setPrivacy(false);
            }

            tokenHeader = new MessageTokenHeader(is, prop);

            encSeqNumber = new byte[8];
            readFully(is, encSeqNumber);

            // debug("\n\tRead EncSeq#=" +
            // getHexBytes(encSeqNumber, encSeqNumber.length));

            checksum = new byte[cipherHelper.getChecksumLength()];
            readFully(is, checksum);

            // debug("\n\tRead checksum=" +
            // getHexBytes(checksum, checksum.length));
            // debug("\nLeaving MessageToken.Cons\n");

        } catch (IOException e) {
            throw new GSSException(GSSException.DEFECTIVE_TOKEN, -1,
                getTokenName(tokenId) + ":" + e.getMessage());
        }
    }

    /**
     * Used to obtain the GSSHeader that was at the start of this
     * token.
     */
    public final GSSHeader getGSSHeader() {
        return gssHeader;
    }

    /**
     * Used to obtain the token id that was contained in this token.
     * @return the token id in the token
     */
    public final int getTokenId() {
        return tokenId;
    }

    /**
     * Used to obtain the encrypted sequence number in this token.
     * @return the encrypted sequence number in the token
     */
    public final byte[] getEncSeqNumber() {
        return encSeqNumber;
    }

    /**
     * Used to obtain the checksum that was contained in this token.
     * @return the checksum in the token
     */
    public final byte[] getChecksum() {
        return checksum;
    }

    /**
     * Used to determine if this token contains any encrypted data.
     * @return true if it contains any encrypted data, false if there is only
     * plaintext data or if there is no data.
     */
    public final boolean getConfState() {
        return confState;
    }

    /**
     * Generates the checksum field and the encrypted sequence number
     * field. The encrypted sequence number uses the 8 bytes of the checksum
     * as an initial vector in a fixed DesCbc algorithm.
     *
     * @param prop the MessageProp structure that determines what sort of
     * checksum and sealing algorithm should be used. The lower byte
     * of qop determines the checksum algorithm while the upper byte
     * determines the signing algorithm.
     *       Checksum values are:
     *           0 - default (DES_MAC)
     *           1 - MD5
     *           2 - DES_MD5
     *           3 - DES_MAC
     *           4 - HMAC_SHA1
     *       Sealing values are:
     *           0 - default (DES)
     *           1 - DES
     *           2 - DES3-KD
     *
     * @param optionalHeader an optional header that will be processed first
     * during  checksum calculation
     *
     * @param data the application data to checksum
     * @param offset the offset where the data starts
     * @param len the length of the data
     *
     * @param optionalTrailer an optional trailer that will be processed
     * last during checksum calculation. e.g., padding that should be
     * appended to the application data
     *
     * @throws GSSException if an error occurs in the checksum calculation or
     * encryption sequence number calculation.
     */
    public void genSignAndSeqNumber(MessageProp prop,
                                    byte[] optionalHeader,
                                    byte[] data, int offset, int len,
                                    byte[] optionalTrailer)
        throws GSSException {

        //    debug("Inside MessageToken.genSignAndSeqNumber:\n");

        int qop = prop.getQOP();
        if (qop != 0) {
            qop = 0;
            prop.setQOP(qop);
        }

        if (!confState) {
            prop.setPrivacy(false);
        }

        // Create a token header with the correct sign and seal algorithm
        // values.
        tokenHeader =
            new MessageTokenHeader(tokenId, prop.getPrivacy(), qop);

        // Calculate SGN_CKSUM

        checksum =
            getChecksum(optionalHeader, data, offset, len, optionalTrailer);

        // debug("\n\tCalc checksum=" +
        // getHexBytes(checksum, checksum.length));

        // Calculate SND_SEQ

        seqNumberData = new byte[8];

        // When using this RC4 based encryption type, the sequence number is
        // always sent in big-endian rather than little-endian order.
        if (cipherHelper.isArcFour()) {
            writeBigEndian(seqNumber, seqNumberData);
        } else {
            // for all other etypes
            writeLittleEndian(seqNumber, seqNumberData);
        }
        if (!initiator) {
            seqNumberData[4] = (byte)0xff;
            seqNumberData[5] = (byte)0xff;
            seqNumberData[6] = (byte)0xff;
            seqNumberData[7] = (byte)0xff;
        }

        encSeqNumber = cipherHelper.encryptSeq(checksum, seqNumberData, 0, 8);

        // debug("\n\tCalc seqNum=" +
        //    getHexBytes(seqNumberData, seqNumberData.length));
        // debug("\n\tCalc encSeqNum=" +
        //    getHexBytes(encSeqNumber, encSeqNumber.length));
    }

    /**
     * Verifies that the checksum field and sequence number direction bytes
     * are valid and consistent with the application data.
     *
     * @param optionalHeader an optional header that will be processed first
     * during checksum calculation.
     *
     * @param data the application data
     * @param offset the offset where the data begins
     * @param len the length of the application data
     *
     * @param optionalTrailer an optional trailer that will be processed last
     * during checksum calculation. e.g., padding that should be appended to
     * the application data
     *
     * @throws GSSException if an error occurs in the checksum calculation or
     * encryption sequence number calculation.
     */
    public final boolean verifySignAndSeqNumber(byte[] optionalHeader,
                                        byte[] data, int offset, int len,
                                        byte[] optionalTrailer)
        throws GSSException {
         // debug("\tIn verifySign:\n");

         // debug("\t\tchecksum:   [" + getHexBytes(checksum) + "]\n");

        byte[] myChecksum =
            getChecksum(optionalHeader, data, offset, len, optionalTrailer);

        // debug("\t\tmychecksum: [" + getHexBytes(myChecksum) +"]\n");
        // debug("\t\tchecksum:   [" + getHexBytes(checksum) + "]\n");

        if (MessageDigest.isEqual(checksum, myChecksum)) {

            seqNumberData = cipherHelper.decryptSeq(
                checksum, encSeqNumber, 0, 8);

            // debug("\t\tencSeqNumber:   [" + getHexBytes(encSeqNumber)
            //  + "]\n");
            // debug("\t\tseqNumberData:   [" + getHexBytes(seqNumberData)
            //  + "]\n");

            /*
             * The token from the initiator has direction bytes 0x00 and
             * the token from the acceptor has direction bytes 0xff.
             */
            byte directionByte = 0;
            if (initiator)
                directionByte = (byte) 0xff; // Received token from acceptor

            if ((seqNumberData[4] == directionByte) &&
                  (seqNumberData[5] == directionByte) &&
                  (seqNumberData[6] == directionByte) &&
                  (seqNumberData[7] == directionByte))
                return true;
        }

        return false;

    }

    public final int getSequenceNumber() {
        int sequenceNum = 0;
        if (cipherHelper.isArcFour()) {
            sequenceNum = readBigEndian(seqNumberData, 0, 4);
        } else {
            sequenceNum = readLittleEndian(seqNumberData, 0, 4);
        }
        return sequenceNum;
    }

    /**
     * Computes the checksum based on the algorithm stored in the
     * tokenHeader.
     *
     * @param optionalHeader an optional header that will be processed first
     * during checksum calculation.
     *
     * @param data the application data
     * @param offset the offset where the data begins
     * @param len the length of the application data
     *
     * @param optionalTrailer an optional trailer that will be processed last
     * during checksum calculation. e.g., padding that should be appended to
     * the application data
     *
     * @throws GSSException if an error occurs in the checksum calculation.
     */
    private byte[] getChecksum(byte[] optionalHeader,
                               byte[] data, int offset, int len,
                               byte[] optionalTrailer)
        throws GSSException {

        //      debug("Will do getChecksum:\n");

        /*
         * For checksum calculation the token header bytes i.e., the first 8
         * bytes following the GSSHeader, are logically prepended to the
         * application data to bind the data to this particular token.
         *
         * Note: There is no such requirement wrt adding padding to the
         * application data for checksumming, although the cryptographic
         * algorithm used might itself apply some padding.
         */

        byte[] tokenHeaderBytes = tokenHeader.getBytes();
        byte[] existingHeader = optionalHeader;
        byte[] checksumDataHeader = tokenHeaderBytes;

        if (existingHeader != null) {
            checksumDataHeader = new byte[tokenHeaderBytes.length +
                                         existingHeader.length];
            System.arraycopy(tokenHeaderBytes, 0,
                             checksumDataHeader, 0, tokenHeaderBytes.length);
            System.arraycopy(existingHeader, 0,
                             checksumDataHeader, tokenHeaderBytes.length,
                             existingHeader.length);
        }

        return cipherHelper.calculateChecksum(tokenHeader.getSignAlg(),
             checksumDataHeader, optionalTrailer, data, offset, len, tokenId);
    }


    /**
     * Constructs an empty MessageToken for the local context to send to
     * the peer. It also increments the local sequence number in the
     * Krb5Context instance it uses after obtaining the object lock for
     * it.
     *
     * @param tokenId the token id that should be contained in this token
     * @param context the Kerberos context associated with this token
     */
    MessageToken(int tokenId, Krb5Context context) throws GSSException {
        /*
          debug("\n============================");
          debug("\nMySessionKey=" +
          getHexBytes(context.getMySessionKey().getBytes()));
          debug("\nPeerSessionKey=" +
          getHexBytes(context.getPeerSessionKey().getBytes()));
          debug("\n============================\n");
        */
        init(tokenId, context);
        this.seqNumber = context.incrementMySequenceNumber();
    }

    private void init(int tokenId, Krb5Context context) throws GSSException {
        this.tokenId = tokenId;
        // Just for consistency check in Wrap
        this.confState = context.getConfState();

        this.initiator = context.isInitiator();

        this.cipherHelper = context.getCipherHelper(null);
        //    debug("In MessageToken.Cons");
    }

    /**
     * Encodes a GSSHeader and this token onto an OutputStream.
     *
     * @param os the OutputStream to which this should be written
     * @throws GSSException if an error occurs while writing to the OutputStream
     */
    public void encode(OutputStream os) throws IOException, GSSException {
        gssHeader = new GSSHeader(OID, getKrb5TokenSize());
        gssHeader.encode(os);
        tokenHeader.encode(os);
        // debug("Writing seqNumber: " + getHexBytes(encSeqNumber));
        os.write(encSeqNumber);
        // debug("Writing checksum: " + getHexBytes(checksum));
        os.write(checksum);
    }

    /**
     * Obtains the size of this token. Note that this excludes the size of
     * the GSSHeader.
     * @return token size
     */
    protected int getKrb5TokenSize() throws GSSException {
        return getTokenSize();
    }

    protected final int getTokenSize() throws GSSException {
        return TOKEN_NO_CKSUM_SIZE + cipherHelper.getChecksumLength();
    }

    protected static final int getTokenSize(CipherHelper ch)
        throws GSSException {
         return TOKEN_NO_CKSUM_SIZE + ch.getChecksumLength();
    }

    /**
     * Obtains the conext key that is associated with this token.
     * @return the context key
     */
    /*
    public final byte[] getContextKey() {
        return contextKey;
    }
    */

    /**
     * Obtains the encryption algorithm that should be used in this token
     * given the state of confidentiality the application requested.
     * Requested qop must be consistent with negotiated session key.
     * @param confRequested true if the application desired confidentiality
     * on this token, false otherwise
     * @param qop the qop requested by the application
     * @throws GSSException if qop is incompatible with the negotiated
     *         session key
     */
    protected abstract int getSealAlg(boolean confRequested, int qop)
        throws GSSException;

    // ******************************************* //
    //  I N N E R    C L A S S E S    F O L L O W
    // ******************************************* //

    /**
     * This inner class represents the initial portion of the message token
     * and contains information about the checksum and encryption algorithms
     * that are in use. It constitutes the first 8 bytes of the
     * message token:
     * <pre>
     *     0..1           TOK_ID          Identification field.
     *                                    01 01 - Mic token
     *                                    02 01 - Wrap token
     *     2..3           SGN_ALG         Checksum algorithm indicator.
     *                                    00 00 - DES MAC MD5
     *                                    01 00 - MD2.5
     *                                    02 00 - DES MAC
     *                                    04 00 - HMAC SHA1 DES3-KD
     *                                    11 00 - RC4-HMAC
     *     4..5           SEAL_ALG        ff ff - none
     *                                    00 00 - DES
     *                                    02 00 - DES3-KD
     *                                    10 00 - RC4-HMAC
     *     6..7           Filler          Contains ff ff
     * </pre>
     */
    class MessageTokenHeader {

         private int tokenId;
         private int signAlg;
         private int sealAlg;

         private byte[] bytes = new byte[8];

        /**
         * Constructs a MessageTokenHeader for the specified token type with
         * appropriate checksum and encryption algorithms fields.
         *
         * @param tokenId the token id for this mesage token
         * @param conf true if confidentiality will be resuested with this
         * message token, false otherwise.
         * @param qop the value of the quality of protection that will be
         * desired.
         */
        public MessageTokenHeader(int tokenId, boolean conf, int qop)
         throws GSSException {

            this.tokenId = tokenId;

            signAlg = MessageToken.this.getSgnAlg(qop);

            sealAlg = MessageToken.this.getSealAlg(conf, qop);

            bytes[0] = (byte) (tokenId >>> 8);
            bytes[1] = (byte) (tokenId);

            bytes[2] = (byte) (signAlg >>> 8);
            bytes[3] = (byte) (signAlg);

            bytes[4] = (byte) (sealAlg >>> 8);
            bytes[5] = (byte) (sealAlg);

            bytes[6] = (byte) (MessageToken.FILLER >>> 8);
            bytes[7] = (byte) (MessageToken.FILLER);
        }

        /**
         * Constructs a MessageTokenHeader by reading it from an InputStream
         * and sets the appropriate confidentiality and quality of protection
         * values in a MessageProp structure.
         *
         * @param is the InputStream to read from
         * @param prop the MessageProp to populate
         * @throws IOException is an error occurs while reading from the
         * InputStream
         */
        public MessageTokenHeader(InputStream is, MessageProp prop)
            throws IOException {
            readFully(is, bytes);
            tokenId = readInt(bytes, TOKEN_ID_POS);
            signAlg = readInt(bytes, SIGN_ALG_POS);
            sealAlg = readInt(bytes, SEAL_ALG_POS);
            //          debug("\nMessageTokenHeader read tokenId=" +
            //                getHexBytes(bytes) + "\n");
            // XXX compare to FILLER
            int temp = readInt(bytes, SEAL_ALG_POS + 2);

            //              debug("SIGN_ALG=" + signAlg);

            switch (sealAlg) {
            case SEAL_ALG_DES:
            case SEAL_ALG_DES3_KD:
            case SEAL_ALG_ARCFOUR_HMAC:
                prop.setPrivacy(true);
                break;

            default:
                prop.setPrivacy(false);
            }

            prop.setQOP(0);  // default
        }

        /**
         * Encodes this MessageTokenHeader onto an OutputStream
         * @param os the OutputStream to write to
         * @throws IOException is an error occurs while writing
         */
        public final void encode(OutputStream os) throws IOException {
            os.write(bytes);
        }


        /**
         * Returns the token id for the message token.
         * @return the token id
         * @see sun.security.jgss.krb5.Krb5Token#MIC_ID
         * @see sun.security.jgss.krb5.Krb5Token#WRAP_ID
         */
        public final int getTokenId() {
            return tokenId;
        }

        /**
         * Returns the sign algorithm for the message token.
         * @return the sign algorithm
         * @see sun.security.jgss.krb5.MessageToken#SIGN_DES_MAC
         * @see sun.security.jgss.krb5.MessageToken#SIGN_DES_MAC_MD5
         */
        public final int getSignAlg() {
            return signAlg;
        }

        /**
         * Returns the seal algorithm for the message token.
         * @return the seal algorithm
         * @see sun.security.jgss.krb5.MessageToken#SEAL_ALG_DES
         * @see sun.security.jgss.krb5.MessageToken#SEAL_ALG_NONE
         */
        public final int getSealAlg() {
            return sealAlg;
        }

        /**
         * Returns the bytes of this header.
         * @return 8 bytes that form this header
         */
        public final byte[] getBytes() {
            return bytes;
        }
    } // end of class MessageTokenHeader


    /**
     * Determine signing algorithm based on QOP.
     */
    protected int getSgnAlg(int qop) throws GSSException {
         // QOP ignored
         return cipherHelper.getSgnAlg();
    }
}