/*

Copyright (C) 2015, 2016 Olaf Till <i7tiol@t-online.de>

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 3 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, see <http://www.gnu.org/licenses/>.

*/

#include "parallel-gnutls.h"

#ifdef HAVE_LIBGNUTLS_EXTRA
// The condition in #ifdef is probably only roughly equivalent to what
// is to be tested, but I doubt it's worth to work out a better test
// (we would have to test for being present and being deprecated; if
// deprecated, the function is only a stub). Use
// gnutls_global_set_mem_functions() in gnutls versions still
// implementing it. BTW the later versions currently don't yet seem to
// care for SRP password zeroizing themselves.

extern "C" void zero_free (void *p);

void zero_free (void *p)
{
  if (p) memset (p, 0, malloc_usable_size (p));

  free (p);
}

extern "C" void *zero_realloc (void *p, size_t s);

void *zero_realloc (void *p, size_t s)
{
  if (! p)
    return malloc (s);

  void *np = malloc (s);

  if (! np && s > 0)
    return np;

  int os = malloc_usable_size (p);

  if (np)
    memcpy (np, p, os < s ? os : s);

  memset (p, 0, os);

  free (p);

  return np;
}

void parallel_gnutls_set_mem_functions (void)
{
  gnutls_global_set_mem_functions
    (malloc, malloc, NULL, zero_realloc, zero_free);
}

#endif // HAVE_LIBGNUTLS_EXTRA

static int parse_tpasswd_line (char *line, int nread,
                               const char *username, gnutls_datum_t *salt,
                               gnutls_datum_t *verifier)
{
  int pos;

  char *p;

  gnutls_datum_t b64entry;

  if (! (p = strchr (line, ':')))
    {
      c_verror ("invalid line in password file\n");

      return -1;
    }

  pos = p - line;

  if (! (strncmp (line, username, pos)))
    {
      // get verifier
      if (++pos >= nread)
        {
          c_verror ("invalid line in password file\n");

          return -1;
        }
      if (! (p = strchr (line = p + 1, ':')))
        {
          c_verror ("invalid line in password file\n");

          return -1;
        }
      // base64decode verifier
      b64entry.data = (unsigned char *) line;
      pos += (b64entry.size = p - line) + 1;
      if (gnutls_srp_base64_decode_alloc (&b64entry, verifier))
        {
          c_verror ("could not base64-decode verifier\n");

          return -1;
        }

      // get salt
      if (pos >= nread)
        {
          c_verror ("invalid line in password file\n");

          return -1;
        }
      if (! (p = strchr (line = p + 1, ':')))
        {
          c_verror ("invalid line in password file\n");

          return -1;
        }
      // base64decode salt
      b64entry.data = (unsigned char *) line;
      pos += (b64entry.size = p - line) + 1;
      if (gnutls_srp_base64_decode_alloc (&b64entry, salt))
        {
          c_verror ("could not base64-decode salt\n");

          return -1;
        }

      // check index
      if (pos >= nread)
        {
          c_verror ("invalid line in password file\n");

          return -1;
        }
      if (atoi (p + 1) != 1)
        {
          c_verror ("invalid index in password file\n");

          return -1;
        }

      return 1;
    } // if (! (strncmp (line, username, pos)))
  else
    return 0;
}

#define BUFLEN 2048

int octave_parallel_server_credentials_callback
(gnutls_session_t session, const char *username, gnutls_datum_t *salt,
 gnutls_datum_t *verifier, gnutls_datum_t *g, gnutls_datum_t *n)
{
  /*
    In an initializing call with session == NULL, which is performed
    before setting the function as a callback, the pointer to the
    tpasswd filename is set.
   */

  static const char *passwd = NULL;

  if (! session)
    {
      passwd = username;

      return 0;
    }

  /*
    We have been called as a callback.
  */

  if (! passwd)
    {
      c_verror ("no password file initialized\n");

      return -1;
    }

  struct __bufguard
  {
    char *__b;
    __bufguard (char *__ib) : __b (__ib) { }
    ~__bufguard (void) { memset ((void *) __b, '\0', BUFLEN); delete[] __b; }
    char *__get (void) { return __b; }
  } __bg (new char[BUFLEN]);

  char *line = __bg.__get ();

  if (! line)
    {
      c_verror ("could not allocate memory to read lines of password file\n");

      return -1;
    }

  line[0] = '\0';

  octave_parallel_stream spi
    (new octave_parallel_file_streambuf (passwd, O_RDONLY, 0));

  if (! spi.good ())
    {
      c_verror ("could not open password file\n");

      return -1;
    }

  salt->data = verifier->data = g->data = n->data = NULL;
  salt->size = verifier->size = g->size = n->size = 0;

  int nread, parse_res;

  bool valid_user = false;

  while (true)
    {
      spi.get_istream ().getline (line, BUFLEN);

      nread = strlen (line);

      if (spi.get_istream ().fail ())
        {
          if (nread == BUFLEN - 1)
            c_verror ("line in password file too long\n");
            
          break;
        }

      if (nread == 0)
        {
          c_verror ("empty line in password file\n");

          break;
        }

      if ((parse_res = parse_tpasswd_line (line, nread, username,
                                           salt, verifier)))
        {
          if (parse_res == 1)
            valid_user = true;

          break;
        }
    }

  n->data = gnutls_srp_2048_group_prime.data;
  n->size = gnutls_srp_2048_group_prime.size;

  g->data = gnutls_srp_2048_group_generator.data;
  g->size = gnutls_srp_2048_group_generator.size;

  if (valid_user)
    return 0;
  else
    {
      if (verifier->data)
        {
          gnutls_free (verifier->data);

          verifier->size = 0;
        }

      if (salt->data)
        {
          gnutls_free (salt->data);

          salt->size = 0;
        }

      return 1;
    }
}
