/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include "password-quality-util-pwquality.h"

#if HAVE_PWQUALITY

#include <pwquality.h>
#include <stdio.h>
#include <unistd.h>

#include "alloc-util.h"
#include "dlfcn-util.h"
#include "errno-util.h"
#include "log.h"
#include "password-quality-util.h"
#include "string-util.h"
#include "strv.h"

static void *pwquality_dl = NULL;

static DLSYM_PROTOTYPE(pwquality_check) = NULL;
static DLSYM_PROTOTYPE(pwquality_default_settings) = NULL;
static DLSYM_PROTOTYPE(pwquality_free_settings) = NULL;
static DLSYM_PROTOTYPE(pwquality_generate) = NULL;
static DLSYM_PROTOTYPE(pwquality_get_str_value) = NULL;
static DLSYM_PROTOTYPE(pwquality_read_config) = NULL;
static DLSYM_PROTOTYPE(pwquality_set_int_value) = NULL;
static DLSYM_PROTOTYPE(pwquality_strerror) = NULL;

DEFINE_TRIVIAL_CLEANUP_FUNC_FULL_RENAME(pwquality_settings_t*, sym_pwquality_free_settings, pwquality_free_settingsp, NULL);

static void pwq_maybe_disable_dictionary(pwquality_settings_t *pwq) {
        char buf[PWQ_MAX_ERROR_MESSAGE_LEN];
        const char *path;
        int r;

        assert(pwq);

        r = sym_pwquality_get_str_value(pwq, PWQ_SETTING_DICT_PATH, &path);
        if (r < 0) {
                log_debug("Failed to read libpwquality dictionary path, ignoring: %s",
                          sym_pwquality_strerror(buf, sizeof(buf), r, NULL));
                return;
        }

        if (isempty(path)) {
                log_debug("Weird, no dictionary file configured, ignoring.");
                return;
        }

        if (access(path, F_OK) >= 0)
                return;

        if (errno != ENOENT) {
                log_debug_errno(errno, "Failed to check if dictionary file %s exists, ignoring: %m", path);
                return;
        }

        r = sym_pwquality_set_int_value(pwq, PWQ_SETTING_DICT_CHECK, 0);
        if (r < 0)
                log_debug("Failed to disable libpwquality dictionary check, ignoring: %s",
                          sym_pwquality_strerror(buf, sizeof(buf), r, NULL));
}

static int pwq_allocate_context(pwquality_settings_t **ret) {
        _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL;
        char buf[PWQ_MAX_ERROR_MESSAGE_LEN];
        void *auxerror;
        int r;

        assert(ret);

        r = dlopen_pwquality();
        if (r < 0)
                return r;

        pwq = sym_pwquality_default_settings();
        if (!pwq)
                return -ENOMEM;

        r = sym_pwquality_read_config(pwq, NULL, &auxerror);
        if (r < 0)
                log_debug("Failed to read libpwquality configuration, ignoring: %s",
                          sym_pwquality_strerror(buf, sizeof(buf), r, auxerror));

        pwq_maybe_disable_dictionary(pwq);

        *ret = TAKE_PTR(pwq);
        return 0;
}

int suggest_passwords(void) {
        _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL;
        _cleanup_strv_free_erase_ char **suggestions = NULL;
        _cleanup_(erase_and_freep) char *joined = NULL;
        char buf[PWQ_MAX_ERROR_MESSAGE_LEN];
        int r;

        r = pwq_allocate_context(&pwq);
        if (r < 0) {
                if (ERRNO_IS_NOT_SUPPORTED(r))
                        return 0;
                return log_error_errno(r, "Failed to allocate libpwquality context: %m");
        }

        suggestions = new0(char*, N_SUGGESTIONS+1);
        if (!suggestions)
                return log_oom();

        for (size_t i = 0; i < N_SUGGESTIONS; i++) {
                r = sym_pwquality_generate(pwq, 64, suggestions + i);
                if (r < 0)
                        return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate password, ignoring: %s",
                                               sym_pwquality_strerror(buf, sizeof(buf), r, NULL));
        }

        joined = strv_join(suggestions, " ");
        if (!joined)
                return log_oom();

        printf("Password suggestions: %s\n", joined);
        return 1;
}

int check_password_quality(const char *password, const char *old, const char *username, char **ret_error) {
        _cleanup_(pwquality_free_settingsp) pwquality_settings_t *pwq = NULL;
        char buf[PWQ_MAX_ERROR_MESSAGE_LEN];
        void *auxerror;
        int r;

        assert(password);

        r = pwq_allocate_context(&pwq);
        if (r < 0)
                return log_debug_errno(r, "Failed to allocate libpwquality context: %m");

        r = sym_pwquality_check(pwq, password, old, username, &auxerror);
        if (r < 0) {
                if (ret_error) {
                        r = strdup_to(ret_error,
                                      sym_pwquality_strerror(buf, sizeof(buf), r, auxerror));
                        if (r < 0)
                                return r;
                }

                return 0; /* all bad */
        }

        return 1; /* all good */
}

#endif

int dlopen_pwquality(void) {
#if HAVE_PWQUALITY
        ELF_NOTE_DLOPEN("pwquality",
                        "Support for password quality checks",
                        ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED,
                        "libpwquality.so.1");

        return dlopen_many_sym_or_warn(
                        &pwquality_dl, "libpwquality.so.1", LOG_DEBUG,
                        DLSYM_ARG(pwquality_check),
                        DLSYM_ARG(pwquality_default_settings),
                        DLSYM_ARG(pwquality_free_settings),
                        DLSYM_ARG(pwquality_generate),
                        DLSYM_ARG(pwquality_get_str_value),
                        DLSYM_ARG(pwquality_read_config),
                        DLSYM_ARG(pwquality_set_int_value),
                        DLSYM_ARG(pwquality_strerror));
#else
        return -EOPNOTSUPP;
#endif
}
