/* Copyright (C) 2014 GSS-NTLMSSP contributors, see COPYING for License */

#include <errno.h>
#include <string.h>
#include "gss_ntlmssp.h"


uint32_t gssntlm_cli_auth(uint32_t *minor_status,
                          struct gssntlm_ctx *ctx,
                          struct gssntlm_cred *cred,
                          struct ntlm_buffer *target_info,
                          uint32_t in_flags,
                          gss_channel_bindings_t input_chan_bindings)
{
    struct ntlm_buffer nt_chal_resp = { 0 };
    struct ntlm_buffer lm_chal_resp = { 0 };
    struct ntlm_buffer client_target_info = { 0 };
    struct ntlm_key key_exchange_key = { .length = 16 };
    struct ntlm_key encrypted_random_session_key = { .length = 16 };
    struct ntlm_buffer enc_sess_key = { 0 };
    struct ntlm_buffer auth_mic = { NULL, 16 };
    uint8_t micbuf[16];
    struct ntlm_buffer mic = { micbuf, 16 };
    bool add_mic = false;
    bool key_exch;
    uint32_t retmaj;
    uint32_t retmin;

    switch (cred->type) {

    case GSSNTLM_CRED_USER:

        if (ctx->gss_flags & GSS_C_ANON_FLAG) {
            /* Anonymous auth, empty responses */
            memset(&nt_chal_resp, 0, sizeof(nt_chal_resp));
            lm_chal_resp.data = malloc(1);
            if (!lm_chal_resp.data) {
                set_GSSERR(ENOMEM);
                goto done;
            }
            lm_chal_resp.data[0] = 0;
            lm_chal_resp.length = 1;

        } else if (gssntlm_sec_v2_ok(ctx)) {

            /* ### NTLMv2 ### */
            uint8_t client_chal[8];
            struct ntlm_buffer cli_chal = { client_chal, 8 };
            struct ntlm_key ntlmv2_key = { .length = 16 };
            struct ntlm_buffer nt_proof = { 0 };
            struct ntlm_buffer cb = { 0 };
            uint64_t srv_time = 0;

            if (target_info->length == 0 &&
                input_chan_bindings != GSS_C_NO_CHANNEL_BINDINGS) {
                set_GSSERRS(ERR_NOBINDINGS, GSS_S_BAD_BINDINGS);
                goto done;
            }

            if (target_info->length > 0) {
                bool *add_mic_ptr = NULL;
                bool protect;

                if (input_chan_bindings != GSS_C_NO_CHANNEL_BINDINGS) {
                    if (input_chan_bindings->initiator_addrtype != 0 ||
                        input_chan_bindings->initiator_address.length != 0 ||
                        input_chan_bindings->acceptor_addrtype != 0 ||
                        input_chan_bindings->acceptor_address.length != 0 ||
                        input_chan_bindings->application_data.length == 0) {
                        set_GSSERRS(ERR_BADARG, GSS_S_BAD_BINDINGS);
                        goto done;
                    }
                    cb.length = input_chan_bindings->application_data.length;
                    cb.data = input_chan_bindings->application_data.value;
                }

                protect = in_flags & (NTLMSSP_NEGOTIATE_SIGN
                                      | NTLMSSP_NEGOTIATE_SEAL);
                if (protect) {
                    if (ctx->int_flags & NTLMSSP_CTX_FLAG_SPNEGO_CAN_MIC) {
                        add_mic_ptr = &add_mic;
                    }
                }

                retmin = ntlm_process_target_info(
                                            ctx->ntlm, protect, target_info,
                                            ctx->target_name.data.server.spn,
                                            &cb, &client_target_info,
                                            &srv_time, add_mic_ptr);
                if (retmin) {
                    set_GSSERR(retmin);
                    goto done;
                }

                if (srv_time != 0) {
                    long int tdiff;
                    tdiff = ntlm_timestamp_now() - srv_time;
                    if ((tdiff / 10000000) > MAX_CHALRESP_LIFETIME) {
                        set_GSSERRS(ERR_TIMESKEW, GSS_S_CONTEXT_EXPIRED);
                        goto done;
                    }
                }
            }

            /* Random client challenge */
            retmin = RAND_BUFFER(&cli_chal);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }

            /* NTLMv2 Key */
            retmin = NTOWFv2(ctx->ntlm, &cred->cred.user.nt_hash,
                             cred->cred.user.user.data.user.name,
                             cred->cred.user.user.data.user.domain,
                             &ntlmv2_key);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }

            /* NTLMv2 Response */
            retmin = ntlmv2_compute_nt_response(&ntlmv2_key,
                                                ctx->server_chal, client_chal,
                                                srv_time, &client_target_info,
                                                &nt_chal_resp);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }

            if (target_info->length == 0) {
                /* LMv2 Response
                 * (only sent if challenge response has no target_info) */
                retmin = ntlmv2_compute_lm_response(&ntlmv2_key,
                                                    ctx->server_chal,
                                                    client_chal,
                                                    &lm_chal_resp);
                if (retmin) {
                    set_GSSERR(retmin);
                    goto done;
                }
            }

            /* The NT proof is the first 16 bytes */
            nt_proof.data = nt_chal_resp.data;
            nt_proof.length = 16;

            /* The Session Base Key */
            /* In NTLMv2 the Key Exchange Key is the Session Base Key */
            retmin = ntlmv2_session_base_key(&ntlmv2_key, &nt_proof,
                                             &key_exchange_key);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }
        } else {
            /* ### NTLMv1 ### */
            uint8_t client_chal[8];
            struct ntlm_buffer cli_chal = { client_chal, 8 };
            struct ntlm_key session_base_key = { .length = 16 };
            bool NoLMResponseNTLMv1 = !gssntlm_sec_lm_ok(ctx);
            bool ext_sec;

            nt_chal_resp.length = 24;
            nt_chal_resp.data = calloc(1, nt_chal_resp.length);
            lm_chal_resp.length = 24;
            lm_chal_resp.data = calloc(1, lm_chal_resp.length);
            if (!nt_chal_resp.data || !lm_chal_resp.data) {
                set_GSSERR(ENOMEM);
                goto done;
            }

            /* Random client challenge */
            retmin = RAND_BUFFER(&cli_chal);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }

            ext_sec = (in_flags & NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY);

            retmin = ntlm_compute_nt_response(&cred->cred.user.nt_hash,
                                              ext_sec, ctx->server_chal,
                                              client_chal, &nt_chal_resp);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }

            if (!ext_sec && NoLMResponseNTLMv1) {
                memcpy(lm_chal_resp.data, nt_chal_resp.data, 24);
            } else {
                retmin = ntlm_compute_lm_response(&cred->cred.user.lm_hash,
                                                  ext_sec, ctx->server_chal,
                                                  client_chal, &lm_chal_resp);
                if (retmin) {
                    set_GSSERR(retmin);
                    goto done;
                }
            }

            retmin = ntlm_session_base_key(&cred->cred.user.nt_hash,
                                           &session_base_key);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }

            retmin = KXKEY(ctx->ntlm, ext_sec,
                           (in_flags & NTLMSSP_NEGOTIATE_LM_KEY),
                           (in_flags & NTLMSSP_REQUEST_NON_NT_SESSION_KEY),
                           ctx->server_chal, &cred->cred.user.lm_hash,
                           &session_base_key, &lm_chal_resp,
                           &key_exchange_key);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }
        }

        key_exch = (in_flags & NTLMSSP_NEGOTIATE_KEY_EXCH);

        retmin = ntlm_exported_session_key(&key_exchange_key, key_exch,
                                           &ctx->exported_session_key);
        if (retmin) {
            set_GSSERR(retmin);
            goto done;
        }

        if (key_exch) {
            retmin = ntlm_encrypted_session_key(&key_exchange_key,
                                                &ctx->exported_session_key,
                                                &encrypted_random_session_key);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }
        }

        /* in_flags all verified, assign as current flags */
        ctx->neg_flags |= in_flags;

        enc_sess_key.data = encrypted_random_session_key.data;
        enc_sess_key.length = encrypted_random_session_key.length;

        retmin = ntlm_encode_auth_msg(ctx->ntlm, ctx->neg_flags,
                                      &lm_chal_resp,  &nt_chal_resp,
                                      cred->cred.user.user.data.user.domain,
                                      cred->cred.user.user.data.user.name,
                                      ctx->workstation, &enc_sess_key,
                                      add_mic ? &auth_mic : NULL,
                                      &ctx->auth_msg);
        if (retmin) {
            set_GSSERR(retmin);
            goto done;
        }

        /* Now we need to calculate the MIC, because the MIC is part of the
         * message it protects, ntlm_encode_auth_msg() always add a zeroeth
         * buffer, however it returns in data_mic the pointer to the actual
         * area in the auth_msg that points at the mic, so we can backfill */
        if (add_mic) {
            retmin = ntlm_mic(&ctx->exported_session_key, &ctx->nego_msg,
                              &ctx->chal_msg, &ctx->auth_msg, &mic);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }
            /* now that we have the mic, copy it into the auth message */
            memcpy(auth_mic.data, mic.data, 16);

            /* Make sure SPNEGO gets to know it has to add mechlistMIC too */
            ctx->int_flags |= NTLMSSP_CTX_FLAG_AUTH_WITH_MIC;
        }

        set_GSSERRS(0, GSS_S_COMPLETE);
        break;

    case GSSNTLM_CRED_EXTERNAL:
        retmin = external_cli_auth(ctx, cred, in_flags, input_chan_bindings);
        if (retmin) {
            set_GSSERR(retmin);
            goto done;
        }
        set_GSSERRS(0, GSS_S_COMPLETE);
        break;

    default:
        set_GSSERR(ERR_NOUSRCRED);
    }

done:
    ntlm_free_buffer_data(&client_target_info);
    ntlm_free_buffer_data(&nt_chal_resp);
    ntlm_free_buffer_data(&lm_chal_resp);

    return GSSERR();
}


bool is_ntlm_v1(struct ntlm_buffer *nt_chal_resp)
{
    return (nt_chal_resp->length == 24);
}


uint32_t gssntlm_srv_auth(uint32_t *minor_status,
                          struct gssntlm_ctx *ctx,
                          struct gssntlm_cred *cred,
                          struct ntlm_buffer *nt_chal_resp,
                          struct ntlm_buffer *lm_chal_resp,
                          struct ntlm_key *key_exchange_key)
{
    struct ntlm_key session_base_key = { .length = 16 };
    struct ntlm_key ntlmv2_key = { .length = 16 };
    struct ntlm_buffer nt_proof = { 0 };
    uint32_t retmaj, retmin;
    const char *domstr;
    bool ntlm_v1;
    bool ext_sec;
    int retries;

    if (key_exchange_key->length != 16) {
        return GSSERRS(ERR_KEYLEN, GSS_S_FAILURE);
    }

    ntlm_v1 = is_ntlm_v1(nt_chal_resp);

    if (ntlm_v1 && !gssntlm_sec_lm_ok(ctx) && !gssntlm_sec_ntlm_ok(ctx)) {
        return GSSERRS(ERR_NONTLMV1, GSS_S_FAILURE);
    }

    ext_sec = (ctx->neg_flags & NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY);

    switch (cred->type) {

    case GSSNTLM_CRED_USER:
        if (ntlm_v1) {
            uint8_t client_chal[8] = { 0 };

            if (ext_sec) {
                memcpy(client_chal, lm_chal_resp->data, 8);
            }

            retmin = ntlm_verify_nt_response(nt_chal_resp,
                                             &cred->cred.user.nt_hash,
                                             ext_sec, ctx->server_chal,
                                             client_chal);
            if (retmin && gssntlm_sec_lm_ok(ctx)) {
                retmin = ntlm_verify_lm_response(lm_chal_resp,
                                                 &cred->cred.user.lm_hash,
                                                 ext_sec, ctx->server_chal,
                                                 client_chal);
            }

        } else for (retries = 2; retries > 0; retries--) {

            if (retries == 2) {
                domstr = cred->cred.user.user.data.user.domain;
            } else {
                domstr = NULL;
            }

            /* NTLMv2 Key */
            retmin = NTOWFv2(ctx->ntlm, &cred->cred.user.nt_hash,
                             cred->cred.user.user.data.user.name,
                             domstr, &ntlmv2_key);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }

            /* NTLMv2 Response */
            retmin = ntlmv2_verify_nt_response(nt_chal_resp,
                                               &ntlmv2_key,
                                               ctx->server_chal);
            if (retmin && gssntlm_sec_lm_ok(ctx)) {
                /* LMv2 Response */
                retmin = ntlmv2_verify_lm_response(lm_chal_resp,
                                                   &ntlmv2_key,
                                                   ctx->server_chal);
            }
            if (retmin == 0) break;
        }

        if (retmin) {
            set_GSSERR(retmin);
            goto done;
        }

        if (ntlm_v1) {
            retmin = ntlm_session_base_key(&cred->cred.user.nt_hash,
                                           &session_base_key);
            if (retmin) {
                set_GSSERR(retmin);
                goto done;
            }
            break;
        }

        /* The NT proof is the first 16 bytes */
        nt_proof.data = nt_chal_resp->data;
        nt_proof.length = 16;

        /* The Session Base Key */
        /* In NTLMv2 the Key Exchange Key is the Session Base Key */
        retmin = ntlmv2_session_base_key(&ntlmv2_key, &nt_proof,
                                         &session_base_key);
        if (retmin) {
            set_GSSERR(retmin);
            goto done;
        }
        break;

    case GSSNTLM_CRED_EXTERNAL:
        retmin = external_srv_auth(ctx, cred, nt_chal_resp, lm_chal_resp,
                                   &session_base_key);
        if (retmin) {
            set_GSSERR(retmin);
            goto done;
        }
        break;

    default:
        set_GSSERR(ERR_NOUSRCRED);
        goto done;
    }

    if (ntlm_v1) {
        retmin = KXKEY(ctx->ntlm, ext_sec,
                       (ctx->neg_flags & NTLMSSP_NEGOTIATE_LM_KEY),
                       (ctx->neg_flags & NTLMSSP_REQUEST_NON_NT_SESSION_KEY),
                       ctx->server_chal, &cred->cred.user.lm_hash,
                       &session_base_key, lm_chal_resp,
                       key_exchange_key);
        if (retmin) {
            set_GSSERR(retmin);
            goto done;
        }
    } else {
        memcpy(key_exchange_key->data,
               session_base_key.data, session_base_key.length);
    }

    set_GSSERRS(0, GSS_S_COMPLETE);

done:
    return GSSERR();
}
