/*
 * Copyright (c) 2003, 2010, 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.pkcs11;

import java.util.*;
import java.nio.ByteBuffer;

import java.security.*;

import javax.crypto.SecretKey;

import sun.nio.ch.DirectBuffer;

import sun.security.pkcs11.wrapper.*;
import static sun.security.pkcs11.wrapper.PKCS11Constants.*;

/**
 * MessageDigest implementation class. This class currently supports
 * MD2, MD5, SHA-1, SHA-256, SHA-384, and SHA-512.
 *
 * Note that many digest operations are on fairly small amounts of data
 * (less than 100 bytes total). For example, the 2nd hashing in HMAC or
 * the PRF in TLS. In order to speed those up, we use some buffering to
 * minimize number of the Java->native transitions.
 *
 * @author  Andreas Sterbenz
 * @since   1.5
 */
final class P11Digest extends MessageDigestSpi {

    /* unitialized, fields uninitialized, no session acquired */
    private final static int S_BLANK    = 1;

    // data in buffer, all fields valid, session acquired
    // but digest not initialized
    private final static int S_BUFFERED = 2;

    /* session initialized for digesting */
    private final static int S_INIT     = 3;

    private final static int BUFFER_SIZE = 96;

    // token instance
    private final Token token;

    // algorithm name
    private final String algorithm;

    // mechanism id
    private final long mechanism;

    // length of the digest in bytes
    private final int digestLength;

    // associated session, if any
    private Session session;

    // current state, one of S_* above
    private int state;

    // one byte buffer for the update(byte) method, initialized on demand
    private byte[] oneByte;

    // buffer to reduce number of JNI calls
    private final byte[] buffer;

    // offset into the buffer
    private int bufOfs;

    P11Digest(Token token, String algorithm, long mechanism) {
        super();
        this.token = token;
        this.algorithm = algorithm;
        this.mechanism = mechanism;
        switch ((int)mechanism) {
        case (int)CKM_MD2:
        case (int)CKM_MD5:
            digestLength = 16;
            break;
        case (int)CKM_SHA_1:
            digestLength = 20;
            break;
        case (int)CKM_SHA256:
            digestLength = 32;
            break;
        case (int)CKM_SHA384:
            digestLength = 48;
            break;
        case (int)CKM_SHA512:
            digestLength = 64;
            break;
        default:
            throw new ProviderException("Unknown mechanism: " + mechanism);
        }
        buffer = new byte[BUFFER_SIZE];
        state = S_BLANK;
        engineReset();
    }

    // see JCA spec
    protected int engineGetDigestLength() {
        return digestLength;
    }

    private void cancelOperation() {
        token.ensureValid();
        if (session == null) {
            return;
        }
        if ((state != S_INIT) || (token.explicitCancel == false)) {
            return;
        }
        // need to explicitly "cancel" active op by finishing it
        try {
            token.p11.C_DigestFinal(session.id(), buffer, 0, buffer.length);
        } catch (PKCS11Exception e) {
            throw new ProviderException("cancel() failed", e);
        } finally {
            state = S_BUFFERED;
        }
    }

    private void fetchSession() {
        token.ensureValid();
        if (state == S_BLANK) {
            engineReset();
        }
    }

    // see JCA spec
    protected void engineReset() {
        try {
            cancelOperation();
            bufOfs = 0;
            if (session == null) {
                session = token.getOpSession();
            }
            state = S_BUFFERED;
        } catch (PKCS11Exception e) {
            state = S_BLANK;
            throw new ProviderException("reset() failed, ", e);
        }
    }

    // see JCA spec
    protected byte[] engineDigest() {
        try {
            byte[] digest = new byte[digestLength];
            int n = engineDigest(digest, 0, digestLength);
            return digest;
        } catch (DigestException e) {
            throw new ProviderException("internal error", e);
        }
    }

    // see JCA spec
    protected int engineDigest(byte[] digest, int ofs, int len)
            throws DigestException {
        if (len < digestLength) {
            throw new DigestException("Length must be at least " + digestLength);
        }
        fetchSession();
        try {
            int n;
            if (state == S_BUFFERED) {
                n = token.p11.C_DigestSingle(session.id(),
                        new CK_MECHANISM(mechanism),
                        buffer, 0, bufOfs, digest, ofs, len);
            } else {
                if (bufOfs != 0) {
                    doUpdate(buffer, 0, bufOfs);
                }
                n = token.p11.C_DigestFinal(session.id(), digest, ofs, len);
            }
            if (n != digestLength) {
                throw new ProviderException("internal digest length error");
            }
            return n;
        } catch (PKCS11Exception e) {
            throw new ProviderException("digest() failed", e);
        } finally {
            state = S_BLANK;
            bufOfs = 0;
            session = token.releaseSession(session);
        }
    }

    // see JCA spec
    protected void engineUpdate(byte in) {
        if (oneByte == null) {
            oneByte = new byte[1];
        }
        oneByte[0] = in;
        engineUpdate(oneByte, 0, 1);
    }

    // see JCA spec
    protected void engineUpdate(byte[] in, int ofs, int len) {
        fetchSession();
        if (len <= 0) {
            return;
        }
        if ((bufOfs != 0) && (bufOfs + len > buffer.length)) {
            doUpdate(buffer, 0, bufOfs);
            bufOfs = 0;
        }
        if (bufOfs + len > buffer.length) {
            doUpdate(in, ofs, len);
        } else {
            System.arraycopy(in, ofs, buffer, bufOfs, len);
            bufOfs += len;
        }
    }

    // Called by SunJSSE via reflection during the SSL 3.0 handshake if
    // the master secret is sensitive. We may want to consider making this
    // method public in a future release.
    protected void implUpdate(SecretKey key) throws InvalidKeyException {
        fetchSession();
        if (bufOfs != 0) {
            doUpdate(buffer, 0, bufOfs);
            bufOfs = 0;
        }
        // SunJSSE calls this method only if the key does not have a RAW
        // encoding, i.e. if it is sensitive. Therefore, no point in calling
        // SecretKeyFactory to try to convert it. Just verify it ourselves.
        if (key instanceof P11Key == false) {
            throw new InvalidKeyException("Not a P11Key: " + key);
        }
        P11Key p11Key = (P11Key)key;
        if (p11Key.token != token) {
            throw new InvalidKeyException("Not a P11Key of this provider: " + key);
        }
        try {
            if (state == S_BUFFERED) {
                token.p11.C_DigestInit(session.id(), new CK_MECHANISM(mechanism));
                state = S_INIT;
            }
            token.p11.C_DigestKey(session.id(), p11Key.keyID);
        } catch (PKCS11Exception e) {
            throw new ProviderException("update(SecretKey) failed", e);
        }
    }

    // see JCA spec
    protected void engineUpdate(ByteBuffer byteBuffer) {
        fetchSession();
        int len = byteBuffer.remaining();
        if (len <= 0) {
            return;
        }
        if (byteBuffer instanceof DirectBuffer == false) {
            super.engineUpdate(byteBuffer);
            return;
        }
        long addr = ((DirectBuffer)byteBuffer).address();
        int ofs = byteBuffer.position();
        try {
            if (state == S_BUFFERED) {
                token.p11.C_DigestInit(session.id(), new CK_MECHANISM(mechanism));
                state = S_INIT;
                if (bufOfs != 0) {
                    doUpdate(buffer, 0, bufOfs);
                    bufOfs = 0;
                }
            }
            token.p11.C_DigestUpdate(session.id(), addr + ofs, null, 0, len);
            byteBuffer.position(ofs + len);
        } catch (PKCS11Exception e) {
            throw new ProviderException("update() failed", e);
        }
    }

    private void doUpdate(byte[] in, int ofs, int len) {
        if (len <= 0) {
            return;
        }
        try {
            if (state == S_BUFFERED) {
                token.p11.C_DigestInit(session.id(), new CK_MECHANISM(mechanism));
                state = S_INIT;
            }
            token.p11.C_DigestUpdate(session.id(), 0, in, ofs, len);
        } catch (PKCS11Exception e) {
            throw new ProviderException("update() failed", e);
        }
    }
}
