/*
 * Copyright (c) 2005, 2024 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

/*
 * Copied from OpenJDK with permission.
 */

package org.xbib.net.security.auth;

import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Locale;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.SecretKeySpec;


/**
 * NTLMAuthentication:
 *
 * @author Michael McMahon
 * @author Bill Shannon (adapted for Jakarta Mail)
 */
public class Ntlm {

    private static final Logger logger = Logger.getLogger(Ntlm.class.getName());

    private byte[] type1;
    private byte[] type3;

    private SecretKeyFactory fac;
    private Cipher cipher;
    private MD4 md4;
    private final String hostname;
    private final String ntdomain;
    private final String username;
    private final String password;

    private Mac hmac;

    // NTLM flags, as defined in Microsoft NTLM spec
    // https://msdn.microsoft.com/en-us/library/cc236621.aspx
    private static final int NTLMSSP_NEGOTIATE_UNICODE = 0x00000001;
    private static final int NTLMSSP_NEGOTIATE_OEM = 0x00000002;
    private static final int NTLMSSP_REQUEST_TARGET = 0x00000004;
    private static final int NTLMSSP_NEGOTIATE_SIGN = 0x00000010;
    private static final int NTLMSSP_NEGOTIATE_SEAL = 0x00000020;
    private static final int NTLMSSP_NEGOTIATE_DATAGRAM = 0x00000040;
    private static final int NTLMSSP_NEGOTIATE_LM_KEY = 0x00000080;
    private static final int NTLMSSP_NEGOTIATE_NTLM = 0x00000200;
    private static final int NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED = 0x00001000;
    private static final int NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED = 0x00002000;
    private static final int NTLMSSP_NEGOTIATE_ALWAYS_SIGN = 0x00008000;
    private static final int NTLMSSP_TARGET_TYPE_DOMAIN = 0x00010000;
    private static final int NTLMSSP_TARGET_TYPE_SERVER = 0x00020000;
    private static final int NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY = 0x00080000;
    private static final int NTLMSSP_NEGOTIATE_IDENTIFY = 0x00100000;
    private static final int NTLMSSP_REQUEST_NON_NT_SESSION_KEY = 0x00400000;
    private static final int NTLMSSP_NEGOTIATE_TARGET_INFO = 0x00800000;
    private static final int NTLMSSP_NEGOTIATE_VERSION = 0x02000000;
    private static final int NTLMSSP_NEGOTIATE_128 = 0x20000000;
    private static final int NTLMSSP_NEGOTIATE_KEY_EXCH = 0x40000000;
    private static final int NTLMSSP_NEGOTIATE_56 = 0x80000000;

    private static final byte RESPONSERVERSION = 1;
    private static final byte HIRESPONSERVERSION = 1;
    private static final byte[] Z6 = new byte[]{0, 0, 0, 0, 0, 0};
    private static final byte[] Z4 = new byte[]{0, 0, 0, 0};

    private void init0() {
        type1 = new byte[256];    // hopefully large enough
        type3 = new byte[512];    // ditto
        System.arraycopy(new byte[]{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0, 1}, 0,
                type1, 0, 9);
        System.arraycopy(new byte[]{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0, 3}, 0,
                type3, 0, 9);

        try {
            fac = SecretKeyFactory.getInstance("DES");
            cipher = Cipher.getInstance("DES/ECB/NoPadding");
            md4 = new MD4();
        } catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
            throw new AssertionError(e);
        }
    }

    ;

    /**
     * Create an NTLM authenticator.
     * Username may be specified as domain\\username in the Authenticator.
     * If this notation is not used, then the domain will be taken
     * from the ntdomain parameter.
     *
     * @param    ntdomain    the NT domain
     * @param    hostname    the host name
     * @param    username    the user name
     * @param    password    the password
     */
    public Ntlm(String ntdomain, String hostname, String username,
                String password) {
        int i = hostname.indexOf('.');
        if (i != -1) {
            hostname = hostname.substring(0, i);
        }
        i = username.indexOf('\\');
        if (i != -1) {
            ntdomain = username.substring(0, i).toUpperCase(Locale.ENGLISH);
            username = username.substring(i + 1);
        } else if (ntdomain == null) {
            ntdomain = "";
        }
        this.ntdomain = ntdomain;
        this.hostname = hostname;
        this.username = username;
        this.password = password;
        init0();
    }

    private void copybytes(byte[] dest, int destpos, String src, String enc) {
        try {
            byte[] x = src.getBytes(enc);
            System.arraycopy(x, 0, dest, destpos, x.length);
        } catch (UnsupportedEncodingException e) {
            throw new AssertionError(e);
        }
    }

    // for compatibility, just in case
    public String generateType1Msg(int flags) {
        return generateType1Msg(flags, false);
    }

    public String generateType1Msg(int flags, boolean v2) {
        int dlen = ntdomain.length();
        int type1flags =
                NTLMSSP_NEGOTIATE_UNICODE |
                        NTLMSSP_NEGOTIATE_OEM |
                        NTLMSSP_NEGOTIATE_NTLM |
                        NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED |
                        NTLMSSP_NEGOTIATE_ALWAYS_SIGN |
                        flags;
        if (dlen != 0)
            type1flags |= NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED;
        if (v2)
            type1flags |= NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY;
        writeInt(type1, 12, type1flags);
        type1[28] = (byte) 0x20;    // host name offset
        writeShort(type1, 16, dlen);
        writeShort(type1, 18, dlen);

        int hlen = hostname.length();
        writeShort(type1, 24, hlen);
        writeShort(type1, 26, hlen);

        copybytes(type1, 32, hostname, "iso-8859-1");
        copybytes(type1, hlen + 32, ntdomain, "iso-8859-1");
        writeInt(type1, 20, hlen + 32);

        byte[] msg = new byte[32 + hlen + dlen];
        System.arraycopy(type1, 0, msg, 0, 32 + hlen + dlen);
        if (logger.isLoggable(Level.FINE))
            logger.fine("type 1 message: " + toHex(msg));

        String result = new String(Base64.getEncoder().encode(msg),
                StandardCharsets.ISO_8859_1);
        return result;
    }

    /**
     * Convert a 7 byte array to an 8 byte array (for a des key with parity).
     * Input starts at offset off.
     */
    private byte[] makeDesKey(byte[] input, int off) {
        int[] in = new int[input.length];
        for (int i = 0; i < in.length; i++) {
            in[i] = input[i] < 0 ? input[i] + 256 : input[i];
        }
        byte[] out = new byte[8];
        out[0] = (byte) in[off + 0];
        out[1] = (byte) (((in[off + 0] << 7) & 0xFF) | (in[off + 1] >> 1));
        out[2] = (byte) (((in[off + 1] << 6) & 0xFF) | (in[off + 2] >> 2));
        out[3] = (byte) (((in[off + 2] << 5) & 0xFF) | (in[off + 3] >> 3));
        out[4] = (byte) (((in[off + 3] << 4) & 0xFF) | (in[off + 4] >> 4));
        out[5] = (byte) (((in[off + 4] << 3) & 0xFF) | (in[off + 5] >> 5));
        out[6] = (byte) (((in[off + 5] << 2) & 0xFF) | (in[off + 6] >> 6));
        out[7] = (byte) ((in[off + 6] << 1) & 0xFF);
        return out;
    }

    /**
     * Compute hash-based message authentication code for NTLMv2.
     */
    private byte[] hmacMD5(byte[] key, byte[] text) {
        try {
            if (hmac == null)
                hmac = Mac.getInstance("HmacMD5");
        } catch (NoSuchAlgorithmException ex) {
            throw new AssertionError(ex);
        }
        try {
            byte[] nk = new byte[16];
            System.arraycopy(key, 0, nk, 0, Math.min(key.length, 16));
            SecretKeySpec skey = new SecretKeySpec(nk, "HmacMD5");
            hmac.init(skey);
            return hmac.doFinal(text);
        } catch (InvalidKeyException | RuntimeException ex) {
            throw new AssertionError(ex);
        }
    }

    private byte[] calcLMHash() throws GeneralSecurityException {
        byte[] magic = {0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25};
        byte[] pwb = password.toUpperCase(Locale.ENGLISH).getBytes(
                StandardCharsets.ISO_8859_1);
        byte[] pwb1 = new byte[14];
        int len = password.length();
        if (len > 14)
            len = 14;
        System.arraycopy(pwb, 0, pwb1, 0, len); /* Zero padded */

        DESKeySpec dks1 = new DESKeySpec(makeDesKey(pwb1, 0));
        DESKeySpec dks2 = new DESKeySpec(makeDesKey(pwb1, 7));

        SecretKey key1 = fac.generateSecret(dks1);
        SecretKey key2 = fac.generateSecret(dks2);
        cipher.init(Cipher.ENCRYPT_MODE, key1);
        byte[] out1 = cipher.doFinal(magic, 0, 8);
        cipher.init(Cipher.ENCRYPT_MODE, key2);
        byte[] out2 = cipher.doFinal(magic, 0, 8);

        byte[] result = new byte[21];
        System.arraycopy(out1, 0, result, 0, 8);
        System.arraycopy(out2, 0, result, 8, 8);
        return result;
    }

    private byte[] calcNTHash() throws GeneralSecurityException {
        byte[] pw = null;
        try {
            pw = password.getBytes("UnicodeLittleUnmarked");
        } catch (UnsupportedEncodingException e) {
            throw new AssertionError(e);
        }
        byte[] out = md4.digest(pw);
        byte[] result = new byte[21];
        System.arraycopy(out, 0, result, 0, 16);
        return result;
    }

    /*
     * Key is a 21 byte array.  Split it into 3 7 byte chunks,
     * convert each to 8 byte DES keys, encrypt the text arg with
     * each key and return the three results in a sequential [].
     */
    private byte[] calcResponse(byte[] key, byte[] text)
            throws GeneralSecurityException {
        if (key.length != 21) {
            throw new AssertionError();
        }
        DESKeySpec dks1 = new DESKeySpec(makeDesKey(key, 0));
        DESKeySpec dks2 = new DESKeySpec(makeDesKey(key, 7));
        DESKeySpec dks3 = new DESKeySpec(makeDesKey(key, 14));
        SecretKey key1 = fac.generateSecret(dks1);
        SecretKey key2 = fac.generateSecret(dks2);
        SecretKey key3 = fac.generateSecret(dks3);
        cipher.init(Cipher.ENCRYPT_MODE, key1);
        byte[] out1 = cipher.doFinal(text, 0, 8);
        cipher.init(Cipher.ENCRYPT_MODE, key2);
        byte[] out2 = cipher.doFinal(text, 0, 8);
        cipher.init(Cipher.ENCRYPT_MODE, key3);
        byte[] out3 = cipher.doFinal(text, 0, 8);
        byte[] result = new byte[24];
        System.arraycopy(out1, 0, result, 0, 8);
        System.arraycopy(out2, 0, result, 8, 8);
        System.arraycopy(out3, 0, result, 16, 8);
        return result;
    }

    /*
     * Calculate the NTLMv2 response based on the nthash, additional data,
     * and the original challenge.
     */
    private byte[] calcV2Response(byte[] nthash, byte[] blob, byte[] challenge)
            throws GeneralSecurityException {
        byte[] txt = null;
        try {
            txt = (username.toUpperCase(Locale.ENGLISH) + ntdomain).
                    getBytes("UnicodeLittleUnmarked");
        } catch (UnsupportedEncodingException ex) {
            throw new AssertionError(ex);
        }
        byte[] ntlmv2hash = hmacMD5(nthash, txt);
        byte[] cb = new byte[blob.length + 8];
        System.arraycopy(challenge, 0, cb, 0, 8);
        System.arraycopy(blob, 0, cb, 8, blob.length);
        byte[] result = new byte[blob.length + 16];
        System.arraycopy(hmacMD5(ntlmv2hash, cb), 0, result, 0, 16);
        System.arraycopy(blob, 0, result, 16, blob.length);
        return result;
    }

    public String generateType3Msg(String type2msg) {
        try {

            /* First decode the type2 message to get the server challenge */
            /* challenge is located at type2[24] for 8 bytes */
            byte[] type2 = Base64.getDecoder().decode(
                    type2msg.getBytes(StandardCharsets.US_ASCII));
            if (logger.isLoggable(Level.FINE))
                logger.fine("type 2 message: " + toHex(type2));

            byte[] challenge = new byte[8];
            System.arraycopy(type2, 24, challenge, 0, 8);

            int type3flags =
                    NTLMSSP_NEGOTIATE_UNICODE |
                            NTLMSSP_NEGOTIATE_NTLM |
                            NTLMSSP_NEGOTIATE_ALWAYS_SIGN;

            int ulen = username.length() * 2;
            writeShort(type3, 36, ulen);
            writeShort(type3, 38, ulen);
            int dlen = ntdomain.length() * 2;
            writeShort(type3, 28, dlen);
            writeShort(type3, 30, dlen);
            int hlen = hostname.length() * 2;
            writeShort(type3, 44, hlen);
            writeShort(type3, 46, hlen);

            int l = 64;
            copybytes(type3, l, ntdomain, "UnicodeLittleUnmarked");
            writeInt(type3, 32, l);
            l += dlen;
            copybytes(type3, l, username, "UnicodeLittleUnmarked");
            writeInt(type3, 40, l);
            l += ulen;
            copybytes(type3, l, hostname, "UnicodeLittleUnmarked");
            writeInt(type3, 48, l);
            l += hlen;

            int flags = readInt(type2, 20);
            byte[] lmresponse;
            byte[] ntresponse;

            // did the server agree to NTLMv2?
            if ((flags & NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) != 0) {
                // yes, create an NTLMv2 response
                logger.fine("Using NTLMv2");
                type3flags |= NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY;
                byte[] nonce = new byte[8];
                // XXX - allow user to specify Random instance via properties?
                (new Random()).nextBytes(nonce);
                byte[] nthash = calcNTHash();
                lmresponse = calcV2Response(nthash, nonce, challenge);
                byte[] targetInfo = new byte[0];
                if ((flags & NTLMSSP_NEGOTIATE_TARGET_INFO) != 0) {
                    int tlen = readShort(type2, 40);
                    int toff = readInt(type2, 44);
                    targetInfo = new byte[tlen];
                    System.arraycopy(type2, toff, targetInfo, 0, tlen);
                }
                byte[] blob = new byte[32 + targetInfo.length];
                blob[0] = RESPONSERVERSION;
                blob[1] = HIRESPONSERVERSION;
                System.arraycopy(Z6, 0, blob, 2, 6);
                // convert time to NT format
                long now = (System.currentTimeMillis() + 11644473600000L) * 10000L;
                for (int i = 0; i < 8; i++) {
                    blob[8 + i] = (byte) (now & 0xff);
                    now >>= 8;
                }
                System.arraycopy(nonce, 0, blob, 16, 8);
                System.arraycopy(Z4, 0, blob, 24, 4);
                System.arraycopy(targetInfo, 0, blob, 28, targetInfo.length);
                System.arraycopy(Z4, 0, blob, 28 + targetInfo.length, 4);
                ntresponse = calcV2Response(nthash, blob, challenge);
            } else {
                byte[] lmhash = calcLMHash();
                lmresponse = calcResponse(lmhash, challenge);
                byte[] nthash = calcNTHash();
                ntresponse = calcResponse(nthash, challenge);
            }
            System.arraycopy(lmresponse, 0, type3, l, lmresponse.length);
            writeShort(type3, 12, lmresponse.length);
            writeShort(type3, 14, lmresponse.length);
            writeInt(type3, 16, l);
            l += 24;
            System.arraycopy(ntresponse, 0, type3, l, ntresponse.length);
            writeShort(type3, 20, ntresponse.length);
            writeShort(type3, 22, ntresponse.length);
            writeInt(type3, 24, l);
            l += ntresponse.length;
            writeShort(type3, 56, l);
            writeInt(type3, 60, type3flags);

            byte[] msg = new byte[l];
            System.arraycopy(type3, 0, msg, 0, l);

            if (logger.isLoggable(Level.FINE))
                logger.fine("type 3 message: " + toHex(msg));

            String result = new String(Base64.getEncoder().encode(msg),
                    StandardCharsets.ISO_8859_1);
            return result;

        } catch (GeneralSecurityException ex) {
            // should never happen
            logger.log(Level.FINE, "GeneralSecurityException", ex);
            return "";    // will fail later
        }
    }

    private static int readShort(byte[] b, int off) {
        return (((int) b[off]) & 0xff) |
                ((((int) b[off + 1]) & 0xff) << 8);
    }

    private void writeShort(byte[] b, int off, int data) {
        b[off] = (byte) (data & 0xff);
        b[off + 1] = (byte) ((data >> 8) & 0xff);
    }

    private static int readInt(byte[] b, int off) {
        return (((int) b[off]) & 0xff) |
                ((((int) b[off + 1]) & 0xff) << 8) |
                ((((int) b[off + 2]) & 0xff) << 16) |
                ((((int) b[off + 3]) & 0xff) << 24);
    }

    private void writeInt(byte[] b, int off, int data) {
        b[off] = (byte) (data & 0xff);
        b[off + 1] = (byte) ((data >> 8) & 0xff);
        b[off + 2] = (byte) ((data >> 16) & 0xff);
        b[off + 3] = (byte) ((data >> 24) & 0xff);
    }

    private static final char[] hex =
            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

    private static String toHex(byte[] b) {
        StringBuilder sb = new StringBuilder(b.length * 3);
        for (int i = 0; i < b.length; i++)
            sb.append(hex[(b[i] >> 4) & 0xF]).append(hex[b[i] & 0xF]).append(' ');
        return sb.toString();
    }
}
