/*
 * SPDX-FileCopyrightText: 2003-2026 Sébastien Helleu <flashcode@flashtux.org>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

/* SASL authentication with IRC server */

#include <limits.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <gcrypt.h>

#include <gnutls/gnutls.h>
#include <gnutls/abstract.h>

#include "../weechat-plugin.h"
#include "irc.h"
#include "irc-sasl.h"
#include "irc-server.h"


/*
 * These names are sent to the IRC server (as upper case), so they must be
 * valid values for the AUTHENTICATE command (example: "AUTHENTICATE PLAIN").
 */
char *irc_sasl_mechanism_string[IRC_NUM_SASL_MECHANISMS] =
{ "plain", "scram-sha-1", "scram-sha-256", "scram-sha-512",
  "ecdsa-nist256p-challenge", "external" };


/*
 * Build answer for SASL authentication, using mechanism "PLAIN".
 *
 * Note: result must be freed after use.
 */

char *
irc_sasl_mechanism_plain (const char *sasl_username, const char *sasl_password)
{
    char *answer_base64, *string;
    int length_username, length;

    if (!sasl_username || !sasl_password)
        return NULL;

    if (weechat_asprintf (&string,
                          "%s|%s|%s",
                          sasl_username,
                          sasl_username,
                          sasl_password) < 0)
    {
        return NULL;
    }

    length = strlen (string);

    length_username = strlen (sasl_username);
    string[length_username] = '\0';
    string[(length_username * 2) + 1] = '\0';

    answer_base64 = malloc ((length * 4) + 1);
    if (answer_base64)
    {
        if (weechat_string_base_encode ("64", string, length, answer_base64) < 0)
        {
            free (answer_base64);
            answer_base64 = NULL;
        }
    }

    free (string);

    return answer_base64;
}

/*
 * Build answer for SASL authentication, using mechanism
 * "SCRAM-SHA-1", "SCRAM-SHA-256" or "SCRAM-SHA-512".
 *
 * If an error occurs or is received from the server, and if sasl_error is
 * not NULL, *sasl_error is set to the error and must be freed after use.
 *
 * Note: result must be freed after use.
 */

char *
irc_sasl_mechanism_scram (struct t_irc_server *server,
                          const char *hash_algo,
                          const char *data_base64,
                          const char *sasl_username,
                          const char *sasl_password,
                          char **sasl_error)
{
    char *answer_base64, *string, *username, *username2, *data, **attrs;
    char nonce_client[18], nonce_client_base64[24 + 1], *nonce_server;
    char *salt_base64, *salt, *verifier_base64, *verifier, *attr_error;
    char *auth_no_proof, *auth_message;
    char salted_password[512 / 8], client_key[512 / 8], stored_key[512 / 8];
    char client_signature[512 / 8], client_proof[512 / 8];
    char client_proof_base64[((512 / 8) * 4) + 1], server_key[512 / 8];
    char server_signature[512 / 8];
    int i, rc, length, num_attrs, iterations, number;
    int salt_size, salted_password_size;
    int client_key_size, stored_key_size, client_signature_size;
    int server_key_size, server_signature_size, verifier_size;

    if (!server || !hash_algo || !data_base64 || !sasl_username
        || !sasl_password)
    {
        return NULL;
    }

    answer_base64 = NULL;
    string = NULL;
    length = 0;
    username = NULL;
    username2 = NULL;
    data = NULL;
    attrs = NULL;
    nonce_server = NULL;
    salt_base64 = NULL;
    salt = NULL;
    iterations = 0;
    verifier_base64 = NULL;
    verifier = NULL;
    attr_error = NULL;
    auth_no_proof = NULL;
    auth_message = NULL;

    if (strcmp (data_base64, "+") == 0)
    {
        /* Send username and nonce with form: "n,,n=username,r=nonce". */
        gcry_create_nonce (nonce_client, sizeof (nonce_client));
        length = weechat_string_base_encode (
            "64",
            nonce_client, sizeof (nonce_client),
            nonce_client_base64);
        if (length != sizeof (nonce_client_base64) - 1)
            goto base64_encode_error;
        username = weechat_string_replace (sasl_username, "=", "=3D");
        if (!username)
            goto memory_error;
        username2 = weechat_string_replace (username, ",", "=2C");
        if (!username2)
            goto memory_error;
        length = 5 + strlen (username2) + 3 + sizeof (nonce_client_base64) - 1;
        if (weechat_asprintf (&string,
                              "n,,n=%s,r=%s",
                              username2,
                              nonce_client_base64) >= 0)
        {
            free (server->sasl_scram_client_first);
            server->sasl_scram_client_first = strdup (string + 3);
        }
    }
    else
    {
        /* Decode SCRAM attributes sent by the server. */
        data = malloc (strlen (data_base64) + 1);
        if (!data)
            goto memory_error;
        if (weechat_string_base_decode ("64", data_base64, data) <= 0)
            goto base64_decode_error;

        /* Split attributes. */
        attrs = weechat_string_split (data, ",", NULL,
                                      WEECHAT_STRING_SPLIT_STRIP_LEFT
                                      | WEECHAT_STRING_SPLIT_STRIP_RIGHT
                                      | WEECHAT_STRING_SPLIT_COLLAPSE_SEPS,
                                      0, &num_attrs);
        if (!attrs)
            goto proto_error;
        for (i = 0; i < num_attrs; i++)
        {
            if (strncmp (attrs[i], "r=", 2) == 0)
            {
                free (nonce_server);
                nonce_server = strdup (attrs[i] + 2);
            }
            else if (strncmp (attrs[i], "s=", 2) == 0)
            {
                free (salt_base64);
                salt_base64 = strdup (attrs[i] + 2);
            }
            else if (strncmp (attrs[i], "i=", 2) == 0)
            {
                if (weechat_util_parse_int (attrs[i] + 2, 10, &number))
                    iterations = number;
            }
            else if (strncmp (attrs[i], "v=", 2) == 0)
            {
                free (verifier_base64);
                verifier_base64 = strdup (attrs[i] + 2);
            }
            else if (strncmp (attrs[i], "e=", 2) == 0)
            {
                free (attr_error);
                attr_error = strdup (attrs[i] + 2);
            }
        }
        if (attr_error)
        {
            if (sasl_error)
                *sasl_error = strdup (attr_error);
            goto end;
        }
        else if (verifier_base64)
        {
            /* Last exchange: we verify the server signature. */
            if (!server->sasl_scram_salted_pwd
                || (server->sasl_scram_salted_pwd_size <= 0)
                || !server->sasl_scram_auth_message)
            {
                goto proto_error;
            }
            verifier = malloc (strlen (verifier_base64) + 1);
            if (!verifier)
                goto memory_error;
            verifier_size = weechat_string_base_decode ("64", verifier_base64,
                                                        verifier);
            if (verifier_size <= 0)
                goto base64_decode_error;
            /* RFC: ServerKey := HMAC(SaltedPassword, "Server Key") */
            if (!weechat_crypto_hmac (server->sasl_scram_salted_pwd,
                                      server->sasl_scram_salted_pwd_size,
                                      IRC_SASL_SCRAM_SERVER_KEY,
                                      strlen (IRC_SASL_SCRAM_SERVER_KEY),
                                      hash_algo,
                                      server_key,
                                      &server_key_size))
                goto crypto_error;
            /* RFC: ServerSignature := HMAC(ServerKey, AuthMessage) */
            if (!weechat_crypto_hmac (server_key,
                                      server_key_size,
                                      server->sasl_scram_auth_message,
                                      strlen (server->sasl_scram_auth_message),
                                      hash_algo,
                                      server_signature,
                                      &server_signature_size))
                goto crypto_error;
            if (verifier_size != server_signature_size)
                goto crypto_error;
            if (memcmp (verifier, server_signature, verifier_size) != 0)
            {
                if (sasl_error)
                {
                    *sasl_error = strdup (
                        _("unable to validate server signature"));
                }
                string = strdup ("*");
                if (!string)
                    goto memory_error;
                length = strlen (string);
                goto end;
            }
            string = strdup ("+");
            if (!string)
                goto memory_error;
            length = strlen (string);
        }
        else
        {
            if (!server->sasl_scram_client_first || !nonce_server
                || !salt_base64 || (iterations <= 0))
            {
                goto proto_error;
            }
            /* Decode salt. */
            salt = malloc (strlen (salt_base64) + 1);
            if (!salt)
                goto memory_error;
            salt_size = weechat_string_base_decode ("64", salt_base64, salt);
            if (salt_size <= 0)
                goto base64_decode_error;
            /* RFC: SaltedPassword := Hi(Normalize(password), salt, i) */
            if (!weechat_crypto_hash_pbkdf2 (sasl_password,
                                             strlen (sasl_password),
                                             hash_algo,
                                             salt, salt_size,
                                             iterations,
                                             salted_password,
                                             &salted_password_size))
                goto crypto_error;
            free (server->sasl_scram_salted_pwd);
            server->sasl_scram_salted_pwd = malloc (salted_password_size);
            if (!server->sasl_scram_salted_pwd)
                goto memory_error;
            memcpy (server->sasl_scram_salted_pwd, salted_password,
                    salted_password_size);
            server->sasl_scram_salted_pwd_size = salted_password_size;
            /* RFC: ClientKey := HMAC(SaltedPassword, "Client Key") */
            if (!weechat_crypto_hmac (salted_password,
                                      salted_password_size,
                                      IRC_SASL_SCRAM_CLIENT_KEY,
                                      strlen (IRC_SASL_SCRAM_CLIENT_KEY),
                                      hash_algo,
                                      client_key,
                                      &client_key_size))
                goto crypto_error;
            /* RFC: StoredKey := H(ClientKey) */
            if (!weechat_crypto_hash (client_key, client_key_size,
                                      hash_algo,
                                      stored_key,
                                      &stored_key_size))
                goto crypto_error;
            /*
             * RFC: AuthMessage := client-first-message-bare + "," +
             *                     server-first-message + "," +
             *                     client-final-message-without-proof
             */
            length = strlen (nonce_server) + 64 + 1;
            auth_no_proof = malloc (length);
            if (!auth_no_proof)
                goto memory_error;
            /* "biws" is "n,," encoded to base64. */
            snprintf (auth_no_proof, length, "c=biws,r=%s",
                      nonce_server);
            length = strlen (server->sasl_scram_client_first) + 1
                + strlen (data) + 1 + strlen (auth_no_proof) + 1;
            auth_message = malloc (length);
            if (!auth_message)
                goto memory_error;
            rc = snprintf (auth_message, length, "%s,%s,%s",
                           server->sasl_scram_client_first,
                           data,
                           auth_no_proof);
            if ((rc < 0) || (rc >= length))
                goto memory_error;
            free (server->sasl_scram_auth_message);
            server->sasl_scram_auth_message = strdup (auth_message);
            /* RFC: ClientSignature := HMAC(StoredKey, AuthMessage) */
            if (!weechat_crypto_hmac (stored_key,
                                      stored_key_size,
                                      auth_message,
                                      strlen (auth_message),
                                      hash_algo,
                                      client_signature,
                                      &client_signature_size))
                goto crypto_error;
            if (client_key_size != client_signature_size)
                goto crypto_error;
            /* RFC: ClientProof := ClientKey XOR ClientSignature */
            for (i = 0; i < client_key_size; i++)
            {
                client_proof[i] = ((unsigned char)client_key[i] ^
                                   (unsigned char)client_signature[i]);
            }
            if (weechat_string_base_encode ("64", client_proof, client_key_size,
                                            client_proof_base64) < 0)
                goto base64_encode_error;
            /* Final message: auth_no_proof + "," + proof */
            length = strlen (auth_no_proof) + 3 + strlen (client_proof_base64);
            if (weechat_asprintf (&string,
                                  "%s,p=%s",
                                  auth_no_proof,
                                  client_proof_base64) < 0)
            {
                goto memory_error;
            }
        }
    }
    goto end;

memory_error:
    if (sasl_error)
        *sasl_error = strdup (_("memory error"));
    goto end;

base64_decode_error:
    if (sasl_error)
        *sasl_error = strdup (_("base64 decode error"));
    goto end;

base64_encode_error:
    if (sasl_error)
        *sasl_error = strdup (_("base64 encode error"));
    goto end;

crypto_error:
    if (sasl_error)
        *sasl_error = strdup (_("cryptography error"));
    goto end;

proto_error:
    if (sasl_error)
        *sasl_error = strdup (_("protocol error"));
    goto end;

end:
    if (string && (length > 0))
    {
        if ((strcmp (string, "+") == 0) || (strcmp (string, "*") == 0))
        {
            answer_base64 = strdup (string);
        }
        else
        {
            answer_base64 = malloc ((length + 1) * 4);
            if (answer_base64)
            {
                if (weechat_string_base_encode ("64", string, length,
                                                answer_base64) < 0)
                {
                    free (answer_base64);
                    answer_base64 = NULL;
                    if (sasl_error)
                        *sasl_error = strdup (_("base64 encode error"));
                }
            }
        }
    }

    free (string);
    free (username);
    free (username2);
    free (data);
    weechat_string_free_split (attrs);
    free (nonce_server);
    free (salt_base64);
    free (salt);
    free (verifier_base64);
    free (verifier);
    free (attr_error);
    free (auth_no_proof);
    free (auth_message);

    return answer_base64;
}

/*
 * Return the content of file with SASL key.
 *
 * If the file is not found and sasl_error is not NULL, *sasl_error is set to
 * the error and must be freed after use.
 *
 * Note: result must be freed after use.
 */

char *
irc_sasl_get_key_content (const char *sasl_key, char **sasl_error)
{
    char *key_path, *content, str_error[4096];
    struct t_hashtable *options;

    if (!sasl_key)
        return NULL;

    content = NULL;

    options = weechat_hashtable_new (
        32,
        WEECHAT_HASHTABLE_STRING,
        WEECHAT_HASHTABLE_STRING,
        NULL, NULL);
    if (options)
        weechat_hashtable_set (options, "directory", "config");
    key_path = weechat_string_eval_path_home (sasl_key, NULL, NULL, options);
    weechat_hashtable_free (options);

    if (key_path)
        content = weechat_file_get_content (key_path);

    if (key_path && !content && sasl_error)
    {
        snprintf (str_error, sizeof (str_error),
                  _("unable to read private key in file \"%s\""),
                  key_path);
        *sasl_error = strdup (str_error);
    }

    free (key_path);

    return content;
}

/*
 * Build answer for SASL authentication, using mechanism
 * "ECDSA-NIST256P-CHALLENGE".
 *
 * If an error occurs and if sasl_error is not NULL, *sasl_error is set to the
 * error and must be freed after use.
 *
 * Note: result must be freed after use.
 */

char *
irc_sasl_mechanism_ecdsa_nist256p_challenge (struct t_irc_server *server,
                                             const char *data_base64,
                                             const char *sasl_username,
                                             const char *sasl_key,
                                             char **sasl_error)
{
    char *answer_base64, *string, *data, str_error[4096];
    int length_data, length_username, length, ret;
    char *str_privkey;
    gnutls_x509_privkey_t x509_privkey;
    gnutls_privkey_t privkey;
    gnutls_datum_t filedatum, decoded_data, signature;
    gnutls_ecc_curve_t curve;
    gnutls_datum_t x, y, k;
    char *pubkey, *pubkey_base64;

    answer_base64 = NULL;
    string = NULL;

    if (strcmp (data_base64, "+") == 0)
    {
        /* Send "username" + '\0' + "username". */
        length_username = strlen (sasl_username);
        length = length_username + 1 + length_username;
        if (weechat_asprintf (&string,
                              "%s|%s",
                              sasl_username,
                              sasl_username) >= 0)
        {
            string[length_username] = '\0';
        }
    }
    else
    {
        /* Sign the challenge with the private key and return the result. */

        /* Decode the challenge. */
        data = malloc (strlen (data_base64) + 1);
        if (!data)
            return NULL;
        length_data = weechat_string_base_decode ("64", data_base64, data);
        if (length_data <= 0)
        {
            if (sasl_error)
                *sasl_error = strdup (_("base64 decode error"));
            free (data);
            return NULL;
        }

        /* Read file with private key. */
        str_privkey = irc_sasl_get_key_content (sasl_key, sasl_error);
        if (!str_privkey)
        {
            free (data);
            return NULL;
        }

        /* Import key. */
        gnutls_x509_privkey_init (&x509_privkey);
        gnutls_privkey_init (&privkey);
        filedatum.data = (unsigned char *)str_privkey;
        filedatum.size = strlen (str_privkey);
        ret = gnutls_x509_privkey_import (x509_privkey, &filedatum,
                                          GNUTLS_X509_FMT_PEM);
        free (str_privkey);
        if (ret != GNUTLS_E_SUCCESS)
        {
            if (sasl_error)
            {
                snprintf (str_error, sizeof (str_error),
                          _("invalid private key file: error %d %s"),
                          ret,
                          gnutls_strerror (ret));
                *sasl_error = strdup (str_error);
            }
            gnutls_x509_privkey_deinit (x509_privkey);
            gnutls_privkey_deinit (privkey);
            free (data);
            return NULL;
        }

        /* Read raw values in key, to display public key. */
        ret = gnutls_x509_privkey_export_ecc_raw (x509_privkey,
                                                  &curve, &x, &y, &k);
        if (ret == GNUTLS_E_SUCCESS)
        {
            pubkey = malloc (x.size + 1);
            if (pubkey)
            {
                pubkey[0] = (y.data[y.size - 1] & 1) ? 0x03 : 0x02;
                memcpy (pubkey + 1, x.data, x.size);
                pubkey_base64 = malloc ((x.size + 1 + 1) * 4);
                if (pubkey_base64)
                {
                    if (weechat_string_base_encode ("64", pubkey, x.size + 1,
                                                    pubkey_base64) >= 0)
                    {
                        weechat_printf (
                            server->buffer,
                            _("%s%s: signing the challenge with ECC public "
                              "key: %s"),
                            weechat_prefix ("network"),
                            IRC_PLUGIN_NAME,
                            pubkey_base64);
                    }
                    free (pubkey_base64);
                }
                free (pubkey);
            }
            gnutls_free (x.data);
            gnutls_free (y.data);
            gnutls_free (k.data);
        }

        /* Import private key in an abstract key structure. */
        ret = gnutls_privkey_import_x509 (privkey, x509_privkey, 0);
        if (ret != GNUTLS_E_SUCCESS)
        {
            if (sasl_error)
            {
                snprintf (str_error, sizeof (str_error),
                          _("unable to import the private key: error %d %s"),
                          ret,
                          gnutls_strerror (ret));
                *sasl_error = strdup (str_error);
            }
            gnutls_x509_privkey_deinit (x509_privkey);
            gnutls_privkey_deinit (privkey);
            free (data);
            return NULL;
        }

        decoded_data.data = (unsigned char *)data;
        decoded_data.size = length_data;
        ret = gnutls_privkey_sign_hash (privkey, GNUTLS_DIG_SHA256, 0,
                                        &decoded_data, &signature);
        if (ret != GNUTLS_E_SUCCESS)
        {
            if (sasl_error)
            {
                snprintf (str_error, sizeof (str_error),
                          _("unable to sign the hashed data: error %d %s"),
                          ret,
                          gnutls_strerror (ret));
                *sasl_error = strdup (str_error);
            }
            gnutls_x509_privkey_deinit (x509_privkey);
            gnutls_privkey_deinit (privkey);
            free (data);
            return NULL;
        }

        gnutls_x509_privkey_deinit (x509_privkey);
        gnutls_privkey_deinit (privkey);

        string = malloc (signature.size);
        if (string)
            memcpy (string, signature.data, signature.size);
        length = signature.size;

        gnutls_free (signature.data);

        free (data);
    }

    if (string && (length > 0))
    {
        answer_base64 = malloc ((length + 1) * 4);
        if (answer_base64)
        {
            if (weechat_string_base_encode ("64", string, length,
                                            answer_base64) < 0)
            {
                free (answer_base64);
                answer_base64 = NULL;
            }
        }
    }

    free (string);

    return answer_base64;
}

/*
 * Build answer for SASL authentication, using mechanism "EXTERNAL".
 *
 * Note: result must be freed after use.
 */

char *
irc_sasl_mechanism_external (const char *sasl_username)
{
    char *answer_base64;
    int length;

    if (!sasl_username || !sasl_username[0])
        return strdup ("+");

    length = strlen (sasl_username);
    answer_base64 = malloc ((length * 4) + 1);
    if (answer_base64)
    {
        if (weechat_string_base_encode ("64", sasl_username, length, answer_base64) < 0)
        {
            free (answer_base64);
            answer_base64 = NULL;
        }
    }

    return answer_base64;
}
