/*
 * Copyright (C) 1996-2023 The Squid Software Foundation and contributors
 *
 * Squid software is distributed under GPLv2+ license and includes
 * contributions from numerous individuals and organizations.
 * Please see the COPYING and CONTRIBUTORS files for details.
 */

/*
 * NDS LDAP helper functions
 * Copied From Samba-3.0.24 pdb_nds.c and trimmed down to the
 * limited functionality needed to access the plain text password only
 *
 * Original copyright & license follows:
 *
 * Copyright (C) Vince Brimhall         2004-2005
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "squid.h"
#include "auth/digest/eDirectory/digest_common.h"
#include "mem/Sensitive.h"

#if _SQUID_WINDOWS_ && !_SQUID_CYGWIN_

#define snprintf _snprintf
#include <windows.h>
#include <winldap.h>
#include <winber.h>
#ifndef LDAPAPI
#define LDAPAPI __cdecl
#endif
#ifdef LDAP_VERSION3
#ifndef LDAP_OPT_X_TLS
#define LDAP_OPT_X_TLS 0x6000
#endif
#define ber_alloc() ber_alloc_t(0)
#endif /* LDAP_VERSION3 */

#else

#include <lber.h>
#include <ldap.h>

#endif
#include <wchar.h>

#include "edir_ldapext.h"

#define NMASLDAP_GET_LOGIN_CONFIG_REQUEST   "2.16.840.1.113719.1.39.42.100.3"
#define NMASLDAP_GET_LOGIN_CONFIG_RESPONSE  "2.16.840.1.113719.1.39.42.100.4"
#define NMASLDAP_SET_PASSWORD_REQUEST       "2.16.840.1.113719.1.39.42.100.11"
#define NMASLDAP_SET_PASSWORD_RESPONSE      "2.16.840.1.113719.1.39.42.100.12"
#define NMASLDAP_GET_PASSWORD_REQUEST       "2.16.840.1.113719.1.39.42.100.13"
#define NMASLDAP_GET_PASSWORD_RESPONSE      "2.16.840.1.113719.1.39.42.100.14"

#define NMAS_LDAP_EXT_VERSION               1

#define SMB_MALLOC_ARRAY(type, nelem)       calloc(nelem, sizeof(type))
#define DEBUG(level, args)

/**********************************************************************
 Take the request BER value and input data items and BER encodes the
 data into the BER value
**********************************************************************/

static int berEncodePasswordData(
    struct berval **requestBV,
    const char    *objectDN,
    const char    *password,
    const char    *password2)
{
    int err = 0, rc=0;
    BerElement *requestBer = nullptr;

    const char    * utf8ObjPtr = nullptr;
    int     utf8ObjSize = 0;
    const char    * utf8PwdPtr = nullptr;
    int     utf8PwdSize = 0;
    const char    * utf8Pwd2Ptr = nullptr;
    int     utf8Pwd2Size = 0;

    /* Convert objectDN and tag strings from Unicode to UTF-8 */
    utf8ObjSize = strlen(objectDN)+1;
    utf8ObjPtr = objectDN;

    if (password != nullptr) {
        utf8PwdSize = strlen(password)+1;
        utf8PwdPtr = password;
    }

    if (password2 != nullptr) {
        utf8Pwd2Size = strlen(password2)+1;
        utf8Pwd2Ptr = password2;
    }

    /* Allocate a BerElement for the request parameters. */
    if ((requestBer = ber_alloc()) == nullptr) {
        err = LDAP_ENCODING_ERROR;
        ber_free(requestBer, 1);
        return err;
    }

    if (password != nullptr && password2 != nullptr) {
        /* BER encode the NMAS Version, the objectDN, and the password */
        rc = ber_printf(requestBer, "{iooo}", NMAS_LDAP_EXT_VERSION, utf8ObjPtr, utf8ObjSize, utf8PwdPtr, utf8PwdSize, utf8Pwd2Ptr, utf8Pwd2Size);
    } else if (password != nullptr) {
        /* BER encode the NMAS Version, the objectDN, and the password */
        rc = ber_printf(requestBer, "{ioo}", NMAS_LDAP_EXT_VERSION, utf8ObjPtr, utf8ObjSize, utf8PwdPtr, utf8PwdSize);
    } else {
        /* BER encode the NMAS Version and the objectDN */
        rc = ber_printf(requestBer, "{io}", NMAS_LDAP_EXT_VERSION, utf8ObjPtr, utf8ObjSize);
    }

    if (rc < 0) {
        err = LDAP_ENCODING_ERROR;
    } else {
        err = 0;
        /* Convert the BER we just built to a berval that we'll send with the extended request. */
        if ((ber_tag_t)ber_flatten(requestBer, requestBV) == LBER_ERROR) {
            err = LDAP_ENCODING_ERROR;
        }
    }

    if (requestBer) {
        ber_free(requestBer, 1);
    }

    return err;
}

/**********************************************************************
 Take the request BER value and input data items and BER encodes the
 data into the BER value
**********************************************************************/

static int berEncodeLoginData(
    struct berval **requestBV,
    char     *objectDN,
    unsigned int  methodIDLen,
    unsigned int *methodID,
    char     *tag,
    size_t   putDataLen,
    void     *putData)
{
    unsigned int elemCnt = methodIDLen / sizeof(unsigned int);

    char    *utf8ObjPtr=nullptr;
    int     utf8ObjSize = 0;

    char    *utf8TagPtr = nullptr;
    int     utf8TagSize = 0;

    utf8ObjPtr = objectDN;
    utf8ObjSize = strlen(utf8ObjPtr)+1;

    utf8TagPtr = tag;
    utf8TagSize = strlen(utf8TagPtr)+1;

    /* Allocate a BerElement for the request parameters. */
    BerElement *requestBer = ber_alloc();
    if (!requestBer)
        return LDAP_ENCODING_ERROR;

    /* BER encode the NMAS Version and the objectDN */
    if (ber_printf(requestBer, "{io", NMAS_LDAP_EXT_VERSION, utf8ObjPtr, utf8ObjSize) < 0) {
        ber_free(requestBer, 1);
        return LDAP_ENCODING_ERROR;
    }

    /* BER encode the MethodID Length and value */
    if (ber_printf(requestBer, "{i{", methodIDLen) < 0) {
        ber_free(requestBer, 1);
        return LDAP_ENCODING_ERROR;
    }

    for (unsigned int i = 0; i < elemCnt; ++i) {
        if (ber_printf(requestBer, "i", methodID[i]) < 0) {
            ber_free(requestBer, 1);
            return LDAP_ENCODING_ERROR;
        }
    }

    if (ber_printf(requestBer, "}}", 0) < 0) {
        ber_free(requestBer, 1);
        return LDAP_ENCODING_ERROR;
    }

    if (putData) {
        /* BER Encode the the tag and data */
        if (ber_printf(requestBer, "oio}", utf8TagPtr, utf8TagSize, putDataLen, putData, putDataLen) < 0) {
            ber_free(requestBer, 1);
            return LDAP_ENCODING_ERROR;
        }
    } else {
        /* BER Encode the the tag */
        if (ber_printf(requestBer, "o}", utf8TagPtr, utf8TagSize) < 0) {
            ber_free(requestBer, 1);
            return LDAP_ENCODING_ERROR;
        }
    }

    /* Convert the BER we just built to a berval that we'll send with the extended request. */
    if (static_cast<ber_tag_t>(ber_flatten(requestBer, requestBV)) == LBER_ERROR) {
        ber_free(requestBer, 1);
        return LDAP_ENCODING_ERROR;
    }

    ber_free(requestBer, 1);
    return 0; /* no error */
}

/**********************************************************************
 Takes the reply BER Value and decodes the NMAS server version and
 return code and if a non null retData buffer was supplied, tries to
 decode the the return data and length
**********************************************************************/

static int berDecodeLoginData(
    struct berval *replyBV,
    int      *serverVersion,
    size_t   *retDataLen,
    void     *retData )
{
    int err = 0;
    BerElement *replyBer = nullptr;
    char    *retOctStr = nullptr;
    size_t  retOctStrLen = 0;

    if ((replyBer = ber_init(replyBV)) == nullptr) {
        err = LDAP_OPERATIONS_ERROR;
    } else if (retData) {
        retOctStrLen = *retDataLen + 1;
        retOctStr = (char*)SMB_MALLOC_ARRAY(char, retOctStrLen);
        if (!retOctStr) {
            err = LDAP_OPERATIONS_ERROR;
        } else if (ber_scanf(replyBer, "{iis}", serverVersion, &err, retOctStr, &retOctStrLen) != LBER_ERROR) {
            if (*retDataLen >= retOctStrLen) {
                memcpy(retData, retOctStr, retOctStrLen);
            } else if (!err) {
                err = LDAP_NO_MEMORY;
            }

            *retDataLen = retOctStrLen;
        } else if (!err) {
            err = LDAP_DECODING_ERROR;
        }
    } else {
        if (ber_scanf(replyBer, "{ii}", serverVersion, &err) == LBER_ERROR) {
            if (!err) {
                err = LDAP_DECODING_ERROR;
            }
        }
    }

    if (replyBer) {
        ber_free(replyBer, 1);
    }

    if (retOctStr != nullptr) {
        memset(retOctStr, 0, retOctStrLen);
        free(retOctStr);
    }

    return err;
}

/**********************************************************************
 Retrieves data in the login configuration of the specified object
 that is tagged with the specified methodID and tag.
**********************************************************************/

static int getLoginConfig(
    LDAP     *ld,
    char     *objectDN,
    unsigned int  methodIDLen,
    unsigned int *methodID,
    char     *tag,
    size_t   *dataLen,
    void     *data )
{
    int     err = 0;
    struct  berval *requestBV = nullptr;
    char    *replyOID = nullptr;
    struct  berval *replyBV = nullptr;
    int     serverVersion = 0;

    /* Validate unicode parameters. */
    if ((strlen(objectDN) == 0) || ld == nullptr) {
        return LDAP_NO_SUCH_ATTRIBUTE;
    }

    err = berEncodeLoginData(&requestBV, objectDN, methodIDLen, methodID, tag, 0, nullptr);
    if (err) {
        ;
    } else if (!err && (err = ldap_extended_operation_s(ld, NMASLDAP_GET_LOGIN_CONFIG_REQUEST,
                              requestBV, nullptr, nullptr, &replyOID, &replyBV))) {
        /* Call the ldap_extended_operation (synchronously) */
        ;
    } else if (!replyOID) {
        /* Make sure there is a return OID */
        err = LDAP_NOT_SUPPORTED;
    } else if (strcmp(replyOID, NMASLDAP_GET_LOGIN_CONFIG_RESPONSE)) {
        /* Is this what we were expecting to get back. */
        err = LDAP_NOT_SUPPORTED;
    } else if (!replyBV) {
        /* Do we have a good returned berval? */

        /* No; returned berval means we experienced a rather drastic error. */
        /* Return operations error. */
        err = LDAP_OPERATIONS_ERROR;
    } else {

        err = berDecodeLoginData(replyBV, &serverVersion, dataLen, data);

        if (serverVersion != NMAS_LDAP_EXT_VERSION) {
            err = LDAP_OPERATIONS_ERROR;
        }
    }

    if (replyBV) {
        ber_bvfree(replyBV);
    }

    /* Free the return OID string if one was returned. */
    if (replyOID) {
        ldap_memfree(replyOID);
    }

    /* Free memory allocated while building the request ber and berval. */
    if (requestBV) {
        ber_bvfree(requestBV);
    }

    /* Return the appropriate error/success code. */
    return err;
}

/**********************************************************************
 Attempts to get the Simple Password
**********************************************************************/

static int nmasldap_get_simple_pwd(
    LDAP     *ld,
    char     *objectDN,
    size_t   pwdLen,
    char     *pwd )
{
    int err = 0;
    unsigned int methodID = 0;
    unsigned int methodIDLen = sizeof(methodID);
    char    tag[] = {'P','A','S','S','W','O','R','D',' ','H','A','S','H',0};
    char    *pwdBuf=nullptr;
    size_t  pwdBufLen, bufferLen;

    bufferLen = pwdBufLen = pwdLen+2;
    pwdBuf = (char*)SMB_MALLOC_ARRAY(char, pwdBufLen); /* digest and null */
    if (pwdBuf == nullptr) {
        return LDAP_NO_MEMORY;
    }

    err = getLoginConfig(ld, objectDN, methodIDLen, &methodID, tag, &pwdBufLen, pwdBuf);
    if (err == 0) {
        if (pwdBufLen !=0) {
            pwdBuf[pwdBufLen] = 0;       /* null terminate */

            switch (pwdBuf[0]) {
            case 1:  /* cleartext password  */
                break;
            case 2:  /* SHA1 HASH */
            case 3:  /* MD5_ID */
            case 4:  /* UNIXCrypt_ID */
            case 8:  /* SSHA_ID */
            default: /* Unknown digest */
                err = LDAP_INAPPROPRIATE_AUTH;  /* only return clear text */
                break;
            }

            if (!err) {
                if (pwdLen >= pwdBufLen-1) {
                    memcpy(pwd, &pwdBuf[1], pwdBufLen-1);  /* skip digest tag and include null */
                } else {
                    err = LDAP_NO_MEMORY;
                }
            }
        }
    }

    if (pwdBuf != nullptr) {
        Mem::ZeroSensitiveMemory(pwdBuf, bufferLen);
        free(pwdBuf);
    }

    return err;
}

/**********************************************************************
 Attempts to get the Universal Password
**********************************************************************/

static int nmasldap_get_password(
    LDAP     *ld,
    char     *objectDN,
    size_t   *pwdSize,  /* in bytes */
    unsigned char     *pwd )
{
    int err = 0;

    struct berval *requestBV = nullptr;
    char *replyOID = nullptr;
    struct berval *replyBV = nullptr;
    int serverVersion;
    char *pwdBuf;
    size_t pwdBufLen, bufferLen;

    /* Validate char parameters. */
    if (objectDN == nullptr || (strlen(objectDN) == 0) || pwdSize == nullptr || ld == nullptr) {
        return LDAP_NO_SUCH_ATTRIBUTE;
    }

    bufferLen = pwdBufLen = *pwdSize;
    pwdBuf = (char*)SMB_MALLOC_ARRAY(char, pwdBufLen+2);
    if (pwdBuf == nullptr) {
        return LDAP_NO_MEMORY;
    }

    err = berEncodePasswordData(&requestBV, objectDN, nullptr, nullptr);
    if (err) {
        ;
    } else if ((err = ldap_extended_operation_s(ld, NMASLDAP_GET_PASSWORD_REQUEST, requestBV, nullptr, nullptr, &replyOID, &replyBV))) {
        ; /* Call the ldap_extended_operation (synchronously) */
    } else if (!replyOID) {
        /* Make sure there is a return OID */
        err = LDAP_NOT_SUPPORTED;
    } else if (strcmp(replyOID, NMASLDAP_GET_PASSWORD_RESPONSE)) {
        /* Is this what we were expecting to get back. */
        err = LDAP_NOT_SUPPORTED;
    } else if (!replyBV) {
        /* Do we have a good returned berval? */
        /* No; returned berval means we experienced a rather drastic error. */
        /* Return operations error. */
        err = LDAP_OPERATIONS_ERROR;
    } else {
        err = berDecodeLoginData(replyBV, &serverVersion, &pwdBufLen, pwdBuf);

        if (serverVersion != NMAS_LDAP_EXT_VERSION) {
            err = LDAP_OPERATIONS_ERROR;

        } else if (!err && pwdBufLen != 0) {
            if (*pwdSize >= pwdBufLen+1 && pwd != nullptr) {
                memcpy(pwd, pwdBuf, pwdBufLen);
                pwd[pwdBufLen] = 0; /* add null termination */
            }
            *pwdSize = pwdBufLen; /* does not include null termination */
        }
    }

    if (replyBV) {
        ber_bvfree(replyBV);
    }

    /* Free the return OID string if one was returned. */
    if (replyOID) {
        ldap_memfree(replyOID);
    }

    /* Free memory allocated while building the request ber and berval. */
    if (requestBV) {
        ber_bvfree(requestBV);
    }

    if (pwdBuf != nullptr) {
        Mem::ZeroSensitiveMemory(pwdBuf, bufferLen);
        free(pwdBuf);
    }

    /* Return the appropriate error/success code. */
    return err;
}

/**********************************************************************
 Get the user's password from NDS.
 *********************************************************************/

int nds_get_password(
    LDAP *ld,
    char *object_dn,
    size_t *pwd_len,
    char *pwd )
{
    int rc = -1;

    rc = nmasldap_get_password(ld, object_dn, pwd_len, (unsigned char *)pwd);
    if (rc == LDAP_SUCCESS) {
        DEBUG(5, ("NDS Universal Password retrieved for %s\n", object_dn));
    } else {
        DEBUG(3, ("NDS Universal Password NOT retrieved for %s\n", object_dn));
    }

    if (rc != LDAP_SUCCESS) {
        rc = nmasldap_get_simple_pwd(ld, object_dn, *pwd_len, pwd);
        if (rc == LDAP_SUCCESS) {
            DEBUG(5, ("NDS Simple Password retrieved for %s\n", object_dn));
        } else {
            /* We couldn't get the password */
            DEBUG(3, ("NDS Simple Password NOT retrieved for %s\n", object_dn));
            return LDAP_INVALID_CREDENTIALS;
        }
    }

    /* We got the password */
    return LDAP_SUCCESS;
}

