/* Copyright (c) 2004-2006, Roger Dingledine, Nick Mathewson.
 * Copyright (c) 2007-2024, The Tor Project, Inc. */
/* See LICENSE for licensing information */

/**
 * \file control_auth.c
 * \brief Authentication for Tor's control-socket interface.
 **/

#include "core/or/or.h"
#include "app/config/config.h"
#include "core/mainloop/connection.h"
#include "feature/control/control.h"
#include "feature/control/control_events.h"
#include "feature/control/control_cmd.h"
#include "feature/control/control_auth.h"
#include "feature/control/control_cmd_args_st.h"
#include "feature/control/control_connection_st.h"
#include "feature/control/control_proto.h"
#include "lib/crypt_ops/crypto_rand.h"
#include "lib/crypt_ops/crypto_util.h"
#include "lib/encoding/confline.h"
#include "lib/encoding/kvline.h"
#include "lib/encoding/qstring.h"

#include "lib/crypt_ops/crypto_s2k.h"

/* List of authenticated control connections */
static smartlist_t *control_auth_conns = NULL;

static void
control_add_authenticated_connection(control_connection_t *conn)
{
  if (!control_auth_conns)
    control_auth_conns = smartlist_new();

  smartlist_add(control_auth_conns, conn);

  if (smartlist_len(control_auth_conns) == 1)
    stats_init();
}

void
control_remove_authenticated_connection(const control_connection_t *conn)
{
  if (!control_auth_conns)
    return;

  smartlist_remove(control_auth_conns, conn);

  if (smartlist_len(control_auth_conns) == 0) {
    smartlist_free(control_auth_conns);
    control_auth_conns = NULL;
    stats_clear();
  }
}

/** If we're using cookie-type authentication, how long should our cookies be?
 */
#define AUTHENTICATION_COOKIE_LEN 32

/** If true, we've set authentication_cookie to a secret code and
 * stored it to disk. */
static int authentication_cookie_is_set = 0;
/** If authentication_cookie_is_set, a secret cookie that we've stored to disk
 * and which we're using to authenticate controllers.  (If the controller can
 * read it off disk, it has permission to connect.) */
static uint8_t *authentication_cookie = NULL;

#define SAFECOOKIE_SERVER_TO_CONTROLLER_CONSTANT \
  "Tor safe cookie authentication server-to-controller hash"
#define SAFECOOKIE_CONTROLLER_TO_SERVER_CONSTANT \
  "Tor safe cookie authentication controller-to-server hash"
#define SAFECOOKIE_SERVER_NONCE_LEN DIGEST256_LEN

/** Helper: Return a newly allocated string containing a path to the
 * file where we store our authentication cookie. */
char *
get_controller_cookie_file_name(void)
{
  const or_options_t *options = get_options();
  if (options->CookieAuthFile && strlen(options->CookieAuthFile)) {
    return tor_strdup(options->CookieAuthFile);
  } else {
    return get_datadir_fname("control_auth_cookie");
  }
}

/* Initialize the cookie-based authentication system of the
 * ControlPort. If <b>enabled</b> is 0, then disable the cookie
 * authentication system.  */
int
init_control_cookie_authentication(int enabled)
{
  char *fname = NULL;
  int retval;

  if (!enabled) {
    authentication_cookie_is_set = 0;
    return 0;
  }

  fname = get_controller_cookie_file_name();
  retval = init_cookie_authentication(fname, "", /* no header */
                                      AUTHENTICATION_COOKIE_LEN,
                                   get_options()->CookieAuthFileGroupReadable,
                                      &authentication_cookie,
                                      &authentication_cookie_is_set);
  tor_free(fname);
  return retval;
}

/** Decode the hashed, base64'd passwords stored in <b>passwords</b>.
 * Return a smartlist of acceptable passwords (unterminated strings of
 * length S2K_RFC2440_SPECIFIER_LEN+DIGEST_LEN) on success, or NULL on
 * failure.
 */
smartlist_t *
decode_hashed_passwords(config_line_t *passwords)
{
  char decoded[64];
  config_line_t *cl;
  smartlist_t *sl = smartlist_new();

  tor_assert(passwords);

  for (cl = passwords; cl; cl = cl->next) {
    const char *hashed = cl->value;

    if (!strcmpstart(hashed, "16:")) {
      if (base16_decode(decoded, sizeof(decoded), hashed+3, strlen(hashed+3))
                        != S2K_RFC2440_SPECIFIER_LEN + DIGEST_LEN
          || strlen(hashed+3) != (S2K_RFC2440_SPECIFIER_LEN+DIGEST_LEN)*2) {
        goto err;
      }
    } else {
        if (base64_decode(decoded, sizeof(decoded), hashed, strlen(hashed))
            != S2K_RFC2440_SPECIFIER_LEN+DIGEST_LEN) {
          goto err;
        }
    }
    smartlist_add(sl,
                  tor_memdup(decoded, S2K_RFC2440_SPECIFIER_LEN+DIGEST_LEN));
  }

  return sl;

 err:
  SMARTLIST_FOREACH(sl, char*, cp, tor_free(cp));
  smartlist_free(sl);
  return NULL;
}

const control_cmd_syntax_t authchallenge_syntax = {
   .min_args = 1,
   .max_args = 1,
   .accept_keywords=true,
   .kvline_flags=KV_OMIT_KEYS|KV_QUOTED_QSTRING,
   .store_raw_body=true
};

/** Called when we get an AUTHCHALLENGE command. */
int
handle_control_authchallenge(control_connection_t *conn,
                             const control_cmd_args_t *args)
{
  char *client_nonce;
  size_t client_nonce_len;
  char server_hash[DIGEST256_LEN];
  char server_hash_encoded[HEX_DIGEST256_LEN+1];
  char server_nonce[SAFECOOKIE_SERVER_NONCE_LEN];
  char server_nonce_encoded[(2*SAFECOOKIE_SERVER_NONCE_LEN) + 1];

  if (strcasecmp(smartlist_get(args->args, 0), "SAFECOOKIE")) {
    control_write_endreply(conn, 513,
                           "AUTHCHALLENGE only supports SAFECOOKIE "
                           "authentication");
    goto fail;
  }
  if (!authentication_cookie_is_set) {
    control_write_endreply(conn, 515, "Cookie authentication is disabled");
    goto fail;
  }
  if (args->kwargs == NULL || args->kwargs->next != NULL) {
    control_write_endreply(conn, 512,
                           "Wrong number of arguments for AUTHCHALLENGE");
    goto fail;
  }
  if (strcmp(args->kwargs->key, "")) {
    control_write_endreply(conn, 512,
                           "AUTHCHALLENGE does not accept keyword "
                           "arguments.");
    goto fail;
  }

  bool contains_quote = strchr(args->raw_body, '\"');
  if (contains_quote) {
    /* The nonce was quoted */
    client_nonce = tor_strdup(args->kwargs->value);
    client_nonce_len = strlen(client_nonce);
  } else {
    /* The nonce was should be in hex. */
    const char *hex_nonce = args->kwargs->value;
    client_nonce_len = strlen(hex_nonce) / 2;
    client_nonce = tor_malloc(client_nonce_len);
    if (base16_decode(client_nonce, client_nonce_len, hex_nonce,
                      strlen(hex_nonce)) != (int)client_nonce_len) {
      control_write_endreply(conn, 513, "Invalid base16 client nonce");
      tor_free(client_nonce);
      goto fail;
    }
  }

  crypto_rand(server_nonce, SAFECOOKIE_SERVER_NONCE_LEN);

  /* Now compute and send the server-to-controller response, and the
   * server's nonce. */
  tor_assert(authentication_cookie != NULL);

  {
    size_t tmp_len = (AUTHENTICATION_COOKIE_LEN +
                      client_nonce_len +
                      SAFECOOKIE_SERVER_NONCE_LEN);
    char *tmp = tor_malloc_zero(tmp_len);
    char *client_hash = tor_malloc_zero(DIGEST256_LEN);
    memcpy(tmp, authentication_cookie, AUTHENTICATION_COOKIE_LEN);
    memcpy(tmp + AUTHENTICATION_COOKIE_LEN, client_nonce, client_nonce_len);
    memcpy(tmp + AUTHENTICATION_COOKIE_LEN + client_nonce_len,
           server_nonce, SAFECOOKIE_SERVER_NONCE_LEN);

    crypto_hmac_sha256(server_hash,
                       SAFECOOKIE_SERVER_TO_CONTROLLER_CONSTANT,
                       strlen(SAFECOOKIE_SERVER_TO_CONTROLLER_CONSTANT),
                       tmp,
                       tmp_len);

    crypto_hmac_sha256(client_hash,
                       SAFECOOKIE_CONTROLLER_TO_SERVER_CONSTANT,
                       strlen(SAFECOOKIE_CONTROLLER_TO_SERVER_CONSTANT),
                       tmp,
                       tmp_len);

    conn->safecookie_client_hash = client_hash;

    tor_free(tmp);
  }

  base16_encode(server_hash_encoded, sizeof(server_hash_encoded),
                server_hash, sizeof(server_hash));
  base16_encode(server_nonce_encoded, sizeof(server_nonce_encoded),
                server_nonce, sizeof(server_nonce));

  control_printf_endreply(conn, 250,
                          "AUTHCHALLENGE SERVERHASH=%s SERVERNONCE=%s",
                          server_hash_encoded,
                          server_nonce_encoded);

  tor_free(client_nonce);
  return 0;
 fail:
  connection_mark_for_close(TO_CONN(conn));
  return -1;
}

const control_cmd_syntax_t authenticate_syntax = {
   .max_args = 0,
   .accept_keywords=true,
   .kvline_flags=KV_OMIT_KEYS|KV_QUOTED_QSTRING,
   .store_raw_body=true
};

/** Called when we get an AUTHENTICATE message.  Check whether the
 * authentication is valid, and if so, update the connection's state to
 * OPEN.  Reply with DONE or ERROR.
 */
int
handle_control_authenticate(control_connection_t *conn,
                            const control_cmd_args_t *args)
{
  bool used_quoted_string = false;
  const or_options_t *options = get_options();
  const char *errstr = "Unknown error";
  char *password;
  size_t password_len;
  int bad_cookie=0, bad_password=0;
  smartlist_t *sl = NULL;

  if (args->kwargs == NULL) {
    password = tor_strdup("");
    password_len = 0;
  } else if (args->kwargs->next) {
    control_write_endreply(conn, 512, "Too many arguments to AUTHENTICATE.");
    connection_mark_for_close(TO_CONN(conn));
    return 0;
  } else if (strcmp(args->kwargs->key, "")) {
    control_write_endreply(conn, 512,
                           "AUTHENTICATE does not accept keyword arguments.");
    connection_mark_for_close(TO_CONN(conn));
    return 0;
  } else if (strchr(args->raw_body, '\"')) {
    used_quoted_string = true;
    password = tor_strdup(args->kwargs->value);
    password_len = strlen(password);
  } else {
    const char *hex_passwd = args->kwargs->value;
    password_len = strlen(hex_passwd) / 2;
    password = tor_malloc(password_len+1);
    if (base16_decode(password, password_len+1, hex_passwd, strlen(hex_passwd))
                      != (int) password_len) {
      control_write_endreply(conn, 551,
            "Invalid hexadecimal encoding.  Maybe you tried a plain text "
            "password?  If so, the standard requires that you put it in "
            "double quotes.");
      connection_mark_for_close(TO_CONN(conn));
      tor_free(password);
      return 0;
    }
  }

  if (conn->safecookie_client_hash != NULL) {
    /* The controller has chosen safe cookie authentication; the only
     * acceptable authentication value is the controller-to-server
     * response. */

    tor_assert(authentication_cookie_is_set);

    if (password_len != DIGEST256_LEN) {
      log_warn(LD_CONTROL,
               "Got safe cookie authentication response with wrong length "
               "(%d)", (int)password_len);
      errstr = "Wrong length for safe cookie response.";
      goto err;
    }

    if (tor_memneq(conn->safecookie_client_hash, password, DIGEST256_LEN)) {
      log_warn(LD_CONTROL,
               "Got incorrect safe cookie authentication response");
      errstr = "Safe cookie response did not match expected value.";
      goto err;
    }

    tor_free(conn->safecookie_client_hash);
    goto ok;
  }

  if (!options->CookieAuthentication && !options->HashedControlPassword &&
      !options->HashedControlSessionPassword) {
    /* if Tor doesn't demand any stronger authentication, then
     * the controller can get in with anything. */
    goto ok;
  }

  if (options->CookieAuthentication) {
    int also_password = options->HashedControlPassword != NULL ||
      options->HashedControlSessionPassword != NULL;
    if (password_len != AUTHENTICATION_COOKIE_LEN) {
      if (!also_password) {
        log_warn(LD_CONTROL, "Got authentication cookie with wrong length "
                 "(%d)", (int)password_len);
        errstr = "Wrong length on authentication cookie.";
        goto err;
      }
      bad_cookie = 1;
    } else if (tor_memneq(authentication_cookie, password, password_len)) {
      if (!also_password) {
        log_warn(LD_CONTROL, "Got mismatched authentication cookie");
        errstr = "Authentication cookie did not match expected value.";
        goto err;
      }
      bad_cookie = 1;
    } else {
      goto ok;
    }
  }

  if (options->HashedControlPassword ||
      options->HashedControlSessionPassword) {
    int bad = 0;
    smartlist_t *sl_tmp;
    char received[DIGEST_LEN];
    int also_cookie = options->CookieAuthentication;
    sl = smartlist_new();
    if (options->HashedControlPassword) {
      sl_tmp = decode_hashed_passwords(options->HashedControlPassword);
      if (!sl_tmp)
        bad = 1;
      else {
        smartlist_add_all(sl, sl_tmp);
        smartlist_free(sl_tmp);
      }
    }
    if (options->HashedControlSessionPassword) {
      sl_tmp = decode_hashed_passwords(options->HashedControlSessionPassword);
      if (!sl_tmp)
        bad = 1;
      else {
        smartlist_add_all(sl, sl_tmp);
        smartlist_free(sl_tmp);
      }
    }
    if (bad) {
      if (!also_cookie) {
        log_warn(LD_BUG,
                 "Couldn't decode HashedControlPassword: invalid base16");
        errstr="Couldn't decode HashedControlPassword value in configuration.";
        goto err;
      }
      bad_password = 1;
      SMARTLIST_FOREACH(sl, char *, str, tor_free(str));
      smartlist_free(sl);
      sl = NULL;
    } else {
      SMARTLIST_FOREACH(sl, char *, expected,
      {
        secret_to_key_rfc2440(received,DIGEST_LEN,
                              password,password_len,expected);
        if (tor_memeq(expected + S2K_RFC2440_SPECIFIER_LEN,
                      received, DIGEST_LEN))
          goto ok;
      });
      SMARTLIST_FOREACH(sl, char *, str, tor_free(str));
      smartlist_free(sl);
      sl = NULL;

      if (used_quoted_string)
        errstr = "Password did not match HashedControlPassword value from "
          "configuration";
      else
        errstr = "Password did not match HashedControlPassword value from "
          "configuration. Maybe you tried a plain text password? "
          "If so, the standard requires that you put it in double quotes.";
      bad_password = 1;
      if (!also_cookie)
        goto err;
    }
  }

  /** We only get here if both kinds of authentication failed. */
  tor_assert(bad_password && bad_cookie);
  log_warn(LD_CONTROL, "Bad password or authentication cookie on controller.");
  errstr = "Password did not match HashedControlPassword *or* authentication "
    "cookie.";

 err:
  tor_free(password);
  control_printf_endreply(conn, 515, "Authentication failed: %s", errstr);
  connection_mark_for_close(TO_CONN(conn));
  if (sl) { /* clean up */
    SMARTLIST_FOREACH(sl, char *, str, tor_free(str));
    smartlist_free(sl);
  }
  return 0;
 ok:
  log_info(LD_CONTROL, "Authenticated control connection ("TOR_SOCKET_T_FORMAT
           ")", conn->base_.s);
  send_control_done(conn);
  conn->base_.state = CONTROL_CONN_STATE_OPEN;
  tor_free(password);
  if (sl) { /* clean up */
    SMARTLIST_FOREACH(sl, char *, str, tor_free(str));
    smartlist_free(sl);
  }

  control_add_authenticated_connection(conn);

  return 0;
}

void
control_auth_free_all(void)
{
  if (authentication_cookie) /* Free the auth cookie */
    tor_free(authentication_cookie);
  authentication_cookie_is_set = 0;

  if (control_auth_conns)
    smartlist_free(control_auth_conns);
}
