/*
 * Copyright (c) 2005, 2020, 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.wrapper;

import org.ietf.jgss.*;
import java.security.Provider;
import sun.security.jgss.GSSHeader;
import sun.security.jgss.GSSUtil;
import sun.security.jgss.GSSExceptionImpl;
import sun.security.jgss.spi.*;
import sun.security.util.DerValue;
import sun.security.util.ObjectIdentifier;
import sun.security.jgss.spnego.NegTokenInit;
import sun.security.jgss.spnego.NegTokenTarg;
import javax.security.auth.kerberos.DelegationPermission;
import java.io.*;


/**
 * This class is essentially a wrapper class for the gss_ctx_id_t
 * structure of the native GSS library.
 * @author Valerie Peng
 * @since 1.6
 */
class NativeGSSContext implements GSSContextSpi {

    private static final int GSS_C_DELEG_FLAG = 1;
    private static final int GSS_C_MUTUAL_FLAG = 2;
    private static final int GSS_C_REPLAY_FLAG = 4;
    private static final int GSS_C_SEQUENCE_FLAG = 8;
    private static final int GSS_C_CONF_FLAG = 16;
    private static final int GSS_C_INTEG_FLAG = 32;
    private static final int GSS_C_ANON_FLAG = 64;
    private static final int GSS_C_PROT_READY_FLAG = 128;
    private static final int GSS_C_TRANS_FLAG = 256;

    private static final int NUM_OF_INQUIRE_VALUES = 6;

    private long pContext = 0; // Pointer to the gss_ctx_id_t structure
    private GSSNameElement srcName;
    private GSSNameElement targetName;
    private GSSCredElement cred;
    private boolean isInitiator;
    private boolean isEstablished;
    private Oid actualMech; // Assigned during context establishment

    private ChannelBinding cb;
    private GSSCredElement delegatedCred;
    private int flags;
    private int lifetime = GSSCredential.DEFAULT_LIFETIME;
    private final GSSLibStub cStub;

    private boolean skipDelegPermCheck;
    private boolean skipServicePermCheck;

    // Retrieve the (preferred) mech out of SPNEGO tokens, i.e.
    // NegTokenInit & NegTokenTarg
    private static Oid getMechFromSpNegoToken(byte[] token,
                                              boolean isInitiator)
        throws GSSException {
        Oid mech = null;
        if (isInitiator) {
            GSSHeader header = null;
            try {
                header = new GSSHeader(new ByteArrayInputStream(token));
            } catch (IOException ioe) {
                throw new GSSExceptionImpl(GSSException.FAILURE, ioe);
            }
            int negTokenLen = header.getMechTokenLength();
            byte[] negToken = new byte[negTokenLen];
            System.arraycopy(token, token.length-negTokenLen,
                             negToken, 0, negToken.length);

            NegTokenInit ntok = new NegTokenInit(negToken);
            if (ntok.getMechToken() != null) {
                Oid[] mechList = ntok.getMechTypeList();
                mech = mechList[0];
            }
        } else {
            NegTokenTarg ntok = new NegTokenTarg(token);
            mech = ntok.getSupportedMech();
        }
        return mech;
    }

    // Perform the Service permission check
    private void doServicePermCheck() throws GSSException {
        if (System.getSecurityManager() != null) {
            String action = (isInitiator? "initiate" : "accept");
            // Need to check Service permission for accessing
            // initiator cred for SPNEGO during context establishment
            if (GSSUtil.isSpNegoMech(cStub.getMech()) && isInitiator
                && !isEstablished) {
                if (srcName == null) {
                    // Check by creating default initiator KRB5 cred
                    GSSCredElement tempCred =
                        new GSSCredElement(null, lifetime,
                                           GSSCredential.INITIATE_ONLY,
                                           GSSLibStub.getInstance(GSSUtil.GSS_KRB5_MECH_OID));
                    tempCred.dispose();
                } else {
                    String tgsName = Krb5Util.getTGSName(srcName);
                    Krb5Util.checkServicePermission(tgsName, action);
                }
            }
            String targetStr = targetName.getKrbName();
            Krb5Util.checkServicePermission(targetStr, action);
            skipServicePermCheck = true;
        }
    }

    // Perform the Delegation permission check
    private void doDelegPermCheck() throws GSSException {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            String targetStr = targetName.getKrbName();
            String tgsStr = Krb5Util.getTGSName(targetName);
            StringBuilder sb = new StringBuilder("\"");
            sb.append(targetStr).append("\" \"");
            sb.append(tgsStr).append('\"');
            String krbPrincPair = sb.toString();
            SunNativeProvider.debug("Checking DelegationPermission (" +
                                    krbPrincPair + ")");
            DelegationPermission perm =
                new DelegationPermission(krbPrincPair);
            sm.checkPermission(perm);
            skipDelegPermCheck = true;
        }
    }

    private byte[] retrieveToken(InputStream is, int mechTokenLen)
        throws GSSException {
        try {
            byte[] result = null;
            if (mechTokenLen != -1) {
                // Need to add back the GSS header for a complete GSS token
                SunNativeProvider.debug("Precomputed mechToken length: " +
                                         mechTokenLen);
                GSSHeader gssHeader = new GSSHeader
                    (ObjectIdentifier.of(cStub.getMech().toString()),
                     mechTokenLen);
                ByteArrayOutputStream baos = new ByteArrayOutputStream(600);

                byte[] mechToken = new byte[mechTokenLen];
                int len = is.read(mechToken);
                assert(mechTokenLen == len);
                gssHeader.encode(baos);
                baos.write(mechToken);
                result = baos.toByteArray();
            } else {
                // Must be unparsed GSS token or SPNEGO's NegTokenTarg token
                assert(mechTokenLen == -1);
                DerValue dv = new DerValue(is);
                result = dv.toByteArray();
            }
            SunNativeProvider.debug("Complete Token length: " +
                                    result.length);
            return result;
        } catch (IOException ioe) {
            throw new GSSExceptionImpl(GSSException.FAILURE, ioe);
        }
    }

    // Constructor for context initiator
    NativeGSSContext(GSSNameElement peer, GSSCredElement myCred,
                     int time, GSSLibStub stub) throws GSSException {
        if (peer == null) {
            throw new GSSException(GSSException.FAILURE, 1, "null peer");
        }
        cStub = stub;
        cred = myCred;
        targetName = peer;
        isInitiator = true;
        lifetime = time;

        if (GSSUtil.isKerberosMech(cStub.getMech())) {
            doServicePermCheck();
            if (cred == null) {
                cred = new GSSCredElement(null, lifetime,
                                          GSSCredential.INITIATE_ONLY, cStub);
            }
            srcName = cred.getName();
        }
    }

    // Constructor for context acceptor
    NativeGSSContext(GSSCredElement myCred, GSSLibStub stub)
        throws GSSException {
        cStub = stub;
        cred = myCred;

        if (cred != null) targetName = cred.getName();

        isInitiator = false;
        // Defer Service permission check for default acceptor cred
        // to acceptSecContext()
        if (GSSUtil.isKerberosMech(cStub.getMech()) && targetName != null) {
            doServicePermCheck();
        }

        // srcName and potentially targetName (when myCred is null)
        // will be set in GSSLibStub.acceptContext(...)
    }

    // Constructor for imported context
    NativeGSSContext(long pCtxt, GSSLibStub stub) throws GSSException {
        assert(pContext != 0);
        pContext = pCtxt;
        cStub = stub;

        // Set everything except cred, cb, delegatedCred
        long[] info = cStub.inquireContext(pContext);
        if (info.length != NUM_OF_INQUIRE_VALUES) {
            throw new RuntimeException("Bug w/ GSSLibStub.inquireContext()");
        }
        srcName = new GSSNameElement(info[0], cStub);
        targetName = new GSSNameElement(info[1], cStub);
        isInitiator = (info[2] != 0);
        isEstablished = (info[3] != 0);
        flags = (int) info[4];
        lifetime = (int) info[5];

        // Do Service Permission check when importing SPNEGO context
        // just to be safe
        Oid mech = cStub.getMech();
        if (GSSUtil.isSpNegoMech(mech) || GSSUtil.isKerberosMech(mech)) {
            doServicePermCheck();
        }
    }

    public Provider getProvider() {
        return SunNativeProvider.INSTANCE;
    }

    public byte[] initSecContext(InputStream is, int mechTokenLen)
        throws GSSException {
        byte[] outToken = null;
        if ((!isEstablished) && (isInitiator)) {
            byte[] inToken = null;
            // Ignore the specified input stream on the first call
            if (pContext != 0) {
                inToken = retrieveToken(is, mechTokenLen);
                SunNativeProvider.debug("initSecContext=> inToken len=" +
                    inToken.length);
            }

            if (!getCredDelegState()) skipDelegPermCheck = true;

            if (GSSUtil.isKerberosMech(cStub.getMech()) && !skipDelegPermCheck) {
                doDelegPermCheck();
            }

            long pCred = (cred == null? 0 : cred.pCred);
            outToken = cStub.initContext(pCred, targetName.pName,
                                         cb, inToken, this);
            SunNativeProvider.debug("initSecContext=> outToken len=" +
                (outToken == null ? 0 : outToken.length));

            // Only inspect the token when the permission check
            // has not been performed
            if (GSSUtil.isSpNegoMech(cStub.getMech()) && outToken != null) {
                // WORKAROUND for SEAM bug#6287358
                actualMech = getMechFromSpNegoToken(outToken, true);

                if (GSSUtil.isKerberosMech(actualMech)) {
                    if (!skipServicePermCheck) doServicePermCheck();
                    if (!skipDelegPermCheck) doDelegPermCheck();
                }
            }

            if (isEstablished) {
                if (srcName == null) {
                    srcName = new GSSNameElement
                        (cStub.getContextName(pContext, true), cStub);
                }
                if (cred == null) {
                    cred = new GSSCredElement(srcName, lifetime,
                                              GSSCredential.INITIATE_ONLY,
                                              cStub);
                }
            }
        }
        return outToken;
    }

    public byte[] acceptSecContext(InputStream is, int mechTokenLen)
        throws GSSException {
        byte[] outToken = null;
        if ((!isEstablished) && (!isInitiator)) {
            byte[] inToken = retrieveToken(is, mechTokenLen);
            SunNativeProvider.debug("acceptSecContext=> inToken len=" +
                                    inToken.length);
            long pCred = (cred == null? 0 : cred.pCred);
            outToken = cStub.acceptContext(pCred, cb, inToken, this);
            SunNativeProvider.debug("acceptSecContext=> outToken len=" +
                                    (outToken == null? 0 : outToken.length));

            if (targetName == null) {
                targetName = new GSSNameElement
                    (cStub.getContextName(pContext, false), cStub);
                // Replace the current default acceptor cred now that
                // the context acceptor name is available
                if (cred != null) cred.dispose();
                cred = new GSSCredElement(targetName, lifetime,
                                          GSSCredential.ACCEPT_ONLY, cStub);
            }

            // Only inspect token when the permission check has not
            // been performed
            if (GSSUtil.isSpNegoMech(cStub.getMech()) &&
                (outToken != null) && !skipServicePermCheck) {
                if (GSSUtil.isKerberosMech(getMechFromSpNegoToken
                                           (outToken, false))) {
                    doServicePermCheck();
                }
            }
        }
        return outToken;
    }

    public boolean isEstablished() {
        return isEstablished;
    }

    public void dispose() throws GSSException {
        srcName = null;
        targetName = null;
        cred = null;
        delegatedCred = null;
        if (pContext != 0) {
            pContext = cStub.deleteContext(pContext);
            pContext = 0;
        }
    }

    public int getWrapSizeLimit(int qop, boolean confReq,
                                int maxTokenSize)
        throws GSSException {
        return cStub.wrapSizeLimit(pContext, (confReq? 1:0), qop,
                                   maxTokenSize);
    }

    public byte[] wrap(byte[] inBuf, int offset, int len,
                       MessageProp msgProp) throws GSSException {
        byte[] data = inBuf;
        if ((offset != 0) || (len != inBuf.length)) {
            data = new byte[len];
            System.arraycopy(inBuf, offset, data, 0, len);
        }
        return cStub.wrap(pContext, data, msgProp);
    }
    public void wrap(byte[] inBuf, int offset, int len,
                     OutputStream os, MessageProp msgProp)
        throws GSSException {
        try {
        byte[] result = wrap(inBuf, offset, len, msgProp);
        os.write(result);
        } catch (IOException ioe) {
            throw new GSSExceptionImpl(GSSException.FAILURE, ioe);
        }
    }
    public int wrap(byte[] inBuf, int inOffset, int len, byte[] outBuf,
                    int outOffset, MessageProp msgProp)
        throws GSSException {
        byte[] result = wrap(inBuf, inOffset, len, msgProp);
        System.arraycopy(result, 0, outBuf, outOffset, result.length);
        return result.length;
    }
    public void wrap(InputStream inStream, OutputStream outStream,
                     MessageProp msgProp) throws GSSException {
        try {
            byte[] data = new byte[inStream.available()];
            int length = inStream.read(data);
            byte[] token = wrap(data, 0, length, msgProp);
            outStream.write(token);
        } catch (IOException ioe) {
            throw new GSSExceptionImpl(GSSException.FAILURE, ioe);
        }
    }

    public byte[] unwrap(byte[] inBuf, int offset, int len,
                         MessageProp msgProp)
        throws GSSException {
        if ((offset != 0) || (len != inBuf.length)) {
            byte[] temp = new byte[len];
            System.arraycopy(inBuf, offset, temp, 0, len);
            return cStub.unwrap(pContext, temp, msgProp);
        } else {
            return cStub.unwrap(pContext, inBuf, msgProp);
        }
    }
    public int unwrap(byte[] inBuf, int inOffset, int len,
                      byte[] outBuf, int outOffset,
                      MessageProp msgProp) throws GSSException {
        byte[] result = null;
        if ((inOffset != 0) || (len != inBuf.length)) {
            byte[] temp = new byte[len];
            System.arraycopy(inBuf, inOffset, temp, 0, len);
            result = cStub.unwrap(pContext, temp, msgProp);
        } else {
            result = cStub.unwrap(pContext, inBuf, msgProp);
        }
        System.arraycopy(result, 0, outBuf, outOffset, result.length);
        return result.length;
    }
    public void unwrap(InputStream inStream, OutputStream outStream,
                       MessageProp msgProp) throws GSSException {
        try {
            byte[] wrapped = new byte[inStream.available()];
            int wLength = inStream.read(wrapped);
            byte[] data = unwrap(wrapped, 0, wLength, msgProp);
            outStream.write(data);
            outStream.flush();
        } catch (IOException ioe) {
            throw new GSSExceptionImpl(GSSException.FAILURE, ioe);
        }
    }

    public int unwrap(InputStream inStream,
                      byte[] outBuf, int outOffset,
                      MessageProp msgProp) throws GSSException {
        byte[] wrapped = null;
        int wLength = 0;
        try {
            wrapped = new byte[inStream.available()];
            wLength = inStream.read(wrapped);
            byte[] result = unwrap(wrapped, 0, wLength, msgProp);
        } catch (IOException ioe) {
            throw new GSSExceptionImpl(GSSException.FAILURE, ioe);
        }
        byte[] result = unwrap(wrapped, 0, wLength, msgProp);
        System.arraycopy(result, 0, outBuf, outOffset, result.length);
        return result.length;
    }

    public byte[] getMIC(byte[] in, int offset, int len,
                         MessageProp msgProp) throws GSSException {
        int qop = (msgProp == null? 0:msgProp.getQOP());
        byte[] inMsg = in;
        if ((offset != 0) || (len != in.length)) {
            inMsg = new byte[len];
            System.arraycopy(in, offset, inMsg, 0, len);
        }
        return cStub.getMic(pContext, qop, inMsg);
    }

    public void getMIC(InputStream inStream, OutputStream outStream,
                       MessageProp msgProp) throws GSSException {
        try {
            int length = 0;
            byte[] msg = new byte[inStream.available()];
            length = inStream.read(msg);

            byte[] msgToken = getMIC(msg, 0, length, msgProp);
            if ((msgToken != null) && msgToken.length != 0) {
                outStream.write(msgToken);
            }
        } catch (IOException ioe) {
            throw new GSSExceptionImpl(GSSException.FAILURE, ioe);
        }
    }

    public void verifyMIC(byte[] inToken, int tOffset, int tLen,
                          byte[] inMsg, int mOffset, int mLen,
                          MessageProp msgProp) throws GSSException {
        byte[] token = inToken;
        byte[] msg = inMsg;
        if ((tOffset != 0) || (tLen != inToken.length)) {
            token = new byte[tLen];
            System.arraycopy(inToken, tOffset, token, 0, tLen);
        }
        if ((mOffset != 0) || (mLen != inMsg.length)) {
            msg = new byte[mLen];
            System.arraycopy(inMsg, mOffset, msg, 0, mLen);
        }
        cStub.verifyMic(pContext, token, msg, msgProp);
    }

    public void verifyMIC(InputStream tokStream, InputStream msgStream,
                          MessageProp msgProp) throws GSSException {
        try {
            byte[] msg = new byte[msgStream.available()];
            int mLength = msgStream.read(msg);
            byte[] tok = new byte[tokStream.available()];
            int tLength = tokStream.read(tok);
            verifyMIC(tok, 0, tLength, msg, 0, mLength, msgProp);
        } catch (IOException ioe) {
            throw new GSSExceptionImpl(GSSException.FAILURE, ioe);
        }
    }

    public byte[] export() throws GSSException {
        byte[] result = cStub.exportContext(pContext);
        pContext = 0;
        return result;
    }

    private void changeFlags(int flagMask, boolean isEnable) {
        if (isInitiator && pContext == 0) {
            if (isEnable) {
                flags |= flagMask;
            } else {
                flags &= ~flagMask;
            }
        }
    }
    public void requestMutualAuth(boolean state) throws GSSException {
        changeFlags(GSS_C_MUTUAL_FLAG, state);
    }
    public void requestReplayDet(boolean state) throws GSSException {
        changeFlags(GSS_C_REPLAY_FLAG, state);
    }
    public void requestSequenceDet(boolean state) throws GSSException {
        changeFlags(GSS_C_SEQUENCE_FLAG, state);
    }
    public void requestCredDeleg(boolean state) throws GSSException {
        changeFlags(GSS_C_DELEG_FLAG, state);
    }
    public void requestAnonymity(boolean state) throws GSSException {
        changeFlags(GSS_C_ANON_FLAG, state);
    }
    public void requestConf(boolean state) throws GSSException {
        changeFlags(GSS_C_CONF_FLAG, state);
    }
    public void requestInteg(boolean state) throws GSSException {
        changeFlags(GSS_C_INTEG_FLAG, state);
    }
    public void requestDelegPolicy(boolean state) throws GSSException {
        // Not supported, ignore
    }
    public void requestLifetime(int lifetime) throws GSSException {
        if (isInitiator && pContext == 0) {
            this.lifetime = lifetime;
        }
    }
    public void setChannelBinding(ChannelBinding cb) throws GSSException {
        if (pContext == 0) {
            this.cb = cb;
        }
    }

    private boolean checkFlags(int flagMask) {
        return ((flags & flagMask) != 0);
    }
    public boolean getCredDelegState() {
        return checkFlags(GSS_C_DELEG_FLAG);
    }
    public boolean getMutualAuthState() {
        return checkFlags(GSS_C_MUTUAL_FLAG);
    }
    public boolean getReplayDetState() {
        return checkFlags(GSS_C_REPLAY_FLAG);
    }
    public boolean getSequenceDetState() {
        return checkFlags(GSS_C_SEQUENCE_FLAG);
    }
    public boolean getAnonymityState() {
        return checkFlags(GSS_C_ANON_FLAG);
    }
    public boolean isTransferable() throws GSSException {
        return checkFlags(GSS_C_TRANS_FLAG);
    }
    public boolean isProtReady() {
        return checkFlags(GSS_C_PROT_READY_FLAG);
    }
    public boolean getConfState() {
        return checkFlags(GSS_C_CONF_FLAG);
    }
    public boolean getIntegState() {
        return checkFlags(GSS_C_INTEG_FLAG);
    }
    public boolean getDelegPolicyState() {
        return false;
    }
    public int getLifetime() {
        return cStub.getContextTime(pContext);
    }
    public GSSNameSpi getSrcName() throws GSSException {
        return srcName;
    }
    public GSSNameSpi getTargName() throws GSSException {
        return targetName;
    }
    public Oid getMech() throws GSSException {
        if (isEstablished && actualMech != null) {
            return actualMech;
        } else {
            return cStub.getMech();
        }
    }
    public GSSCredentialSpi getDelegCred() throws GSSException {
        return delegatedCred;
    }
    public boolean isInitiator() {
        return isInitiator;
    }

    @SuppressWarnings("deprecation")
    protected void finalize() throws Throwable {
        dispose();
    }

    public Object inquireSecContext(String type)
            throws GSSException {
        throw new GSSException(GSSException.UNAVAILABLE, -1,
                "Inquire type not supported.");
    }
}
