/* argon2.c - Password module for argon2 */
/* $OpenLDAP$ */
/* This work is part of OpenLDAP Software <http://www.openldap.org/>.
 *
 * Copyright 2017-2022 The OpenLDAP Foundation.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted only as authorized by the OpenLDAP
 * Public License.
 *
 * A copy of this license is available in the file LICENSE in the
 * top-level directory of the distribution or, alternatively, at
 * <http://www.OpenLDAP.org/license.html>.
 */

#include "portable.h"
#ifdef SLAPD_PWMOD_PW_ARGON2
#include "ac/string.h"
#include "lber_pvt.h"
#include "lutil.h"

#include "slap.h"

#include <stdint.h>
#include <stdlib.h>

#ifdef HAVE_LIBARGON2
#include <argon2.h>

/*
 * For now, we hardcode the default values from the argon2 command line tool
 * (as of argon2 release 20161029)
 */
#define SLAPD_ARGON2_ITERATIONS 3
#define SLAPD_ARGON2_MEMORY (1 << 12)
#define SLAPD_ARGON2_PARALLELISM 1
#define SLAPD_ARGON2_SALT_LENGTH 16
#define SLAPD_ARGON2_HASH_LENGTH 32

#else /* !HAVE_LIBARGON2 */
#include <sodium.h>

/*
 * Or libsodium interactive settings
 */
#define SLAPD_ARGON2_ITERATIONS crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE
#define SLAPD_ARGON2_MEMORY (crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE / 1024)
#define SLAPD_ARGON2_PARALLELISM 1
#define SLAPD_ARGON2_SALT_LENGTH crypto_pwhash_argon2id_SALTBYTES
#define SLAPD_ARGON2_HASH_LENGTH 32

#endif

static unsigned long iterations = SLAPD_ARGON2_ITERATIONS;
static unsigned long memory = SLAPD_ARGON2_MEMORY;
static unsigned long parallelism = SLAPD_ARGON2_PARALLELISM;

const struct berval slapd_argon2_scheme = BER_BVC("{ARGON2}");

static int
slapd_argon2_hash(
		const struct berval *scheme,
		const struct berval *passwd,
		struct berval *hash,
		const char **text )
{

	/*
	 * Duplicate these values here so future code which allows
	 * configuration has an easier time.
	 */
	uint32_t salt_length, hash_length;
	char *p;
	int rc = LUTIL_PASSWD_ERR;

#ifdef HAVE_LIBARGON2
	struct berval salt;
	size_t encoded_length;

	salt_length = SLAPD_ARGON2_SALT_LENGTH;
	hash_length = SLAPD_ARGON2_HASH_LENGTH;

	encoded_length = argon2_encodedlen( iterations, memory, parallelism,
			salt_length, hash_length, Argon2_id );

	salt.bv_len = salt_length;
	salt.bv_val = ber_memalloc( salt.bv_len );

	if ( salt.bv_val == NULL ) {
		return LUTIL_PASSWD_ERR;
	}

	if ( lutil_entropy( (unsigned char*)salt.bv_val, salt.bv_len ) ) {
		ber_memfree( salt.bv_val );
		return LUTIL_PASSWD_ERR;
	}

	p = hash->bv_val = ber_memalloc( scheme->bv_len + encoded_length );
	if ( p == NULL ) {
		ber_memfree( salt.bv_val );
		return LUTIL_PASSWD_ERR;
	}

	AC_MEMCPY( p, scheme->bv_val, scheme->bv_len );
	p += scheme->bv_len;

	/*
	 * Do the actual heavy lifting
	 */
	if ( argon2i_hash_encoded( iterations, memory, parallelism,
				passwd->bv_val, passwd->bv_len,
				salt.bv_val, salt_length, hash_length,
				p, encoded_length ) == 0 ) {
		rc = LUTIL_PASSWD_OK;
	}
	hash->bv_len = scheme->bv_len + encoded_length;
	ber_memfree( salt.bv_val );

#else /* !HAVE_LIBARGON2 */
	/* Not exposed by libsodium
	salt_length = SLAPD_ARGON2_SALT_LENGTH;
	hash_length = SLAPD_ARGON2_HASH_LENGTH;
	*/

	p = hash->bv_val = ber_memalloc( scheme->bv_len + crypto_pwhash_STRBYTES );
	if ( p == NULL ) {
		return LUTIL_PASSWD_ERR;
	}

	AC_MEMCPY( hash->bv_val, scheme->bv_val, scheme->bv_len );
	p += scheme->bv_len;

	if ( crypto_pwhash_str_alg( p, passwd->bv_val, passwd->bv_len,
				iterations, memory * 1024,
				crypto_pwhash_ALG_ARGON2ID13 ) == 0 ) {
		hash->bv_len = strlen( hash->bv_val );
		rc = LUTIL_PASSWD_OK;
	}
#endif

	if ( rc ) {
		ber_memfree( hash->bv_val );
		return LUTIL_PASSWD_ERR;
	}

	return LUTIL_PASSWD_OK;
}

static int
slapd_argon2_verify(
		const struct berval *scheme,
		const struct berval *passwd,
		const struct berval *cred,
		const char **text )
{
	int rc = LUTIL_PASSWD_ERR;

#ifdef HAVE_LIBARGON2
	if ( strncmp( passwd->bv_val, "$argon2i$", STRLENOF("$argon2i$") ) == 0 ) {
		rc = argon2i_verify( passwd->bv_val, cred->bv_val, cred->bv_len );
	} else if ( strncmp( passwd->bv_val, "$argon2d$", STRLENOF("$argon2d$") ) == 0 ) {
		rc = argon2d_verify( passwd->bv_val, cred->bv_val, cred->bv_len );
	} else if ( strncmp( passwd->bv_val, "$argon2id$", STRLENOF("$argon2id$") ) == 0 ) {
		rc = argon2id_verify( passwd->bv_val, cred->bv_val, cred->bv_len );
	}
#else /* !HAVE_LIBARGON2 */
	rc = crypto_pwhash_str_verify( passwd->bv_val, cred->bv_val, cred->bv_len );
#endif

	if ( rc ) {
		return LUTIL_PASSWD_ERR;
	}
	return LUTIL_PASSWD_OK;
}

int init_module( int argc, char *argv[] )
{
	int i;

#ifdef HAVE_LIBSODIUM
	if ( sodium_init() == -1 ) {
		return -1;
	}
#endif

	for ( i=0; i < argc; i++ ) {
		char *p;
		unsigned long value;

		switch ( *argv[i] ) {
			case 'm':
				p = strchr( argv[i], '=' );
				if ( !p || lutil_atoulx( &value, p+1, 0 ) ) {
					return -1;
				}
				memory = value;
				break;

			case 't':
				p = strchr( argv[i], '=' );
				if ( !p || lutil_atoulx( &value, p+1, 0 ) ) {
					return -1;
				}
				iterations = value;
				break;

			case 'p':
				p = strchr( argv[i], '=' );
				if ( !p || lutil_atoulx( &value, p+1, 0 ) ) {
					return -1;
				}
				parallelism = value;
				break;

			default:
				return -1;
		}
	}

#ifndef HAVE_LIBARGON2
	/* At the moment, we can only use libargon2 to set parallelism for new
	 * hashes */
	if ( parallelism != SLAPD_ARGON2_PARALLELISM ) {
		Debug( LDAP_DEBUG_ANY, "pw-argon2: "
				"non-default parallelism only supported when linked with "
				"libargon2, got p=%lu\n",
				parallelism );

		if ( (slapMode & SLAP_MODE) != SLAP_TOOL_MODE ||
				slapTool == SLAPPASSWD || slapTool == SLAPTEST ) {
			return 1;
		}
	}
#endif

	return lutil_passwd_add( (struct berval *)&slapd_argon2_scheme,
			slapd_argon2_verify, slapd_argon2_hash );
}
#endif /* SLAPD_OVER_PW_ARGON2 */
