/*
 * Copyright (c) 2019-2021, [Ribose Inc](https://www.ribose.com).
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1.  Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *
 * 2.  Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <stdbool.h>
#include <errno.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <vector>
#include <iterator>
#include <cassert>
#include <ctype.h>
#ifdef _MSC_VER
#include "uniwin.h"
#else
#include <sys/param.h>
#include <unistd.h>
#endif

#ifndef _WIN32
#include <termios.h>
#ifdef HAVE_SYS_RESOURCE_H
#include <sys/resource.h>
#endif
#endif

#ifdef _WIN32
#include <crtdbg.h>
#endif

#include "fficli.h"
#include "str-utils.h"
#include "file-utils.h"
#include "time-utils.h"
#include "defaults.h"

#ifndef RNP_USE_STD_REGEX
#include <regex.h>
#else
#include <regex>
#endif

#ifdef HAVE_SYS_RESOURCE_H
/* When system resource consumption limit controls are available this
 * can be used to attempt to disable core dumps which may leak
 * sensitive data.
 *
 * Returns false if disabling core dumps failed, returns true if disabling
 * core dumps succeeded. errno will be set to the result from setrlimit in
 * the event of failure.
 */
static bool
disable_core_dumps(void)
{
    struct rlimit limit;
    int           error;

    errno = 0;
    memset(&limit, 0, sizeof(limit));
    error = setrlimit(RLIMIT_CORE, &limit);

    if (error == 0) {
        error = getrlimit(RLIMIT_CORE, &limit);
        if (error) {
            ERR_MSG("Warning - cannot turn off core dumps");
            return false;
        } else if (limit.rlim_cur == 0) {
            return true; // disabling core dumps ok
        } else {
            return false; // failed for some reason?
        }
    }
    return false;
}
#endif

#ifdef _WIN32
#include <windows.h>
#include <stdexcept>

static std::vector<std::string>
get_utf8_args()
{
    int       arg_nb;
    wchar_t **arg_w;

    arg_w = CommandLineToArgvW(GetCommandLineW(), &arg_nb);
    if (!arg_w) {
        throw std::runtime_error("CommandLineToArgvW failed");
    }

    try {
        std::vector<std::string> result;
        result.reserve(arg_nb);
        for (int i = 0; i < arg_nb; i++) {
            auto utf8 = wstr_to_utf8(arg_w[i]);
            result.push_back(utf8);
        }
        LocalFree(arg_w);
        return result;
    } catch (...) {
        LocalFree(arg_w);
        throw;
    }
}

void
rnp_win_clear_args(int argc, char **argv)
{
    for (int i = 0; i < argc; i++) {
        if (argv[i]) {
            free(argv[i]);
        }
    }
    delete argv;
}

bool
rnp_win_substitute_cmdline_args(int *argc, char ***argv)
{
    int    argc_utf8 = 0;
    char **argv_utf8_cstrs = NULL;
    try {
        auto argv_utf8_strings = get_utf8_args();
        argc_utf8 = argv_utf8_strings.size();
        *argc = argc_utf8;
        argv_utf8_cstrs = new (std::nothrow) char *[argc_utf8 + 1]();
        if (!argv_utf8_cstrs) {
            throw std::bad_alloc();
        }
        for (int i = 0; i < argc_utf8; i++) {
            auto arg_utf8 = strdup(argv_utf8_strings[i].c_str());
            if (!arg_utf8) {
                throw std::bad_alloc();
            }
            argv_utf8_cstrs[i] = arg_utf8;
        }
        /* argv must be terminated with NULL string */
        argv_utf8_cstrs[argc_utf8] = NULL;
    } catch (...) {
        if (argv_utf8_cstrs) {
            rnp_win_clear_args(argc_utf8, argv_utf8_cstrs);
        }
        throw;
    }
    *argc = argc_utf8;
    *argv = argv_utf8_cstrs;
    return true;
}
#endif

static bool
set_pass_fd(FILE **file, int passfd)
{
    if (!file) {
        return false;
    }
    *file = rnp_fdopen(passfd, "r");
    if (!*file) {
        ERR_MSG("Cannot open fd %d for reading", passfd);
        return false;
    }
    return true;
}

static char *
ptimestr(char *dest, size_t size, time_t t)
{
    struct tm tm = {};
    rnp_gmtime(t, tm);
    (void) snprintf(dest,
                    size,
                    "%s%04d-%02d-%02d",
                    rnp_y2k38_warning(t) ? ">=" : "",
                    tm.tm_year + 1900,
                    tm.tm_mon + 1,
                    tm.tm_mday);
    return dest;
}

static bool
cli_rnp_get_confirmation(const cli_rnp_t *rnp, const char *msg, ...)
{
    char    reply[10];
    va_list ap;

    while (true) {
        va_start(ap, msg);
        vfprintf(rnp->userio_out, msg, ap);
        va_end(ap);
        fprintf(rnp->userio_out, " (y/N) ");
        fflush(rnp->userio_out);

        if (fgets(reply, sizeof(reply), rnp->userio_in) == NULL) {
            return false;
        }

        rnp::strip_eol(reply);

        if (strlen(reply) > 0) {
            if (toupper(reply[0]) == 'Y') {
                return true;
            } else if (toupper(reply[0]) == 'N') {
                return false;
            }

            fprintf(rnp->userio_out, "Sorry, response '%s' not understood.\n", reply);
        } else {
            return false;
        }
    }

    return false;
}

static bool
rnp_ask_filename(const std::string &msg, std::string &res, cli_rnp_t &rnp)
{
    fprintf(rnp.userio_out, "%s", msg.c_str());
    fflush(rnp.userio_out);
    char        fname[128] = {0};
    std::string path;
    do {
        if (!fgets(fname, sizeof(fname), rnp.userio_in)) {
            return false;
        }
        path = path + std::string(fname);
        if (rnp::strip_eol(path)) {
            res = path;
            return true;
        }
        if (path.size() >= 2048) {
            fprintf(rnp.userio_out, "%s", "Too long filename, aborting.");
            fflush(rnp.userio_out);
            return false;
        }
    } while (1);
}

/** @brief checks whether file exists already and asks user for the new filename
 *  @param path output file name with path. May be an empty string, then user is asked for it.
 *  @param res resulting output path will be stored here.
 *  @param rnp initialized cli_rnp_t structure with additional data
 *  @return true on success, or false otherwise (user cancels the operation)
 **/

static bool
rnp_get_output_filename(const std::string &path, std::string &res, cli_rnp_t &rnp)
{
    std::string newpath = path;
    if (newpath.empty() &&
        !rnp_ask_filename("Please enter the output filename: ", newpath, rnp)) {
        return false;
    }

    while (true) {
        if (!rnp_file_exists(newpath.c_str())) {
            res = newpath;
            return true;
        }
        if (rnp.cfg().get_bool(CFG_OVERWRITE) ||
            cli_rnp_get_confirmation(
              &rnp,
              "File '%s' already exists. Would you like to overwrite it?",
              newpath.c_str())) {
            rnp_unlink(newpath.c_str());
            res = newpath;
            return true;
        }

        if (!rnp_ask_filename("Please enter the new filename: ", newpath, rnp)) {
            return false;
        }
        if (newpath.empty()) {
            return false;
        }
    }
}

static bool
stdin_getpass(const char *prompt, char *buffer, size_t size, cli_rnp_t &rnp)
{
#ifndef _WIN32
    struct termios saved_flags, noecho_flags;
    bool           restore_ttyflags = false;
#endif
    bool  ok = false;
    FILE *in = NULL;
    FILE *out = NULL;
    FILE *userio_in = rnp.userio_in ? rnp.userio_in : stdin;

    // validate args
    if (!buffer) {
        goto end;
    }
    // doesn't hurt
    *buffer = '\0';

    if (!rnp.cfg().get_bool(CFG_NOTTY)) {
#ifndef _WIN32
        in = fopen("/dev/tty", "w+ce");
#endif
        out = in;
    }

    if (!in) {
        in = userio_in;
        out = rnp.userio_out ? rnp.userio_out : stdout;
    }

    // TODO: Implement alternative for hiding password entry on Windows
    // TODO: avoid duplicate termios code with pass-provider.cpp
#ifndef _WIN32
    // save the original termios
    if (tcgetattr(fileno(in), &saved_flags) == 0) {
        noecho_flags = saved_flags;
        // disable echo in the local modes
        noecho_flags.c_lflag = (noecho_flags.c_lflag & ~ECHO) | ECHONL | ISIG;
        restore_ttyflags = (tcsetattr(fileno(in), TCSANOW, &noecho_flags) == 0);
    }
#endif
    if (prompt) {
        fputs(prompt, out);
    }
    if (fgets(buffer, size, in) == NULL) {
        goto end;
    }

    rnp::strip_eol(buffer);
    ok = true;
end:
#ifndef _WIN32
    if (restore_ttyflags) {
        tcsetattr(fileno(in), TCSAFLUSH, &saved_flags);
    }
#endif
    if (in && (in != userio_in)) {
        fclose(in);
    }
    return ok;
}

static bool
ffi_pass_callback_stdin(rnp_ffi_t        ffi,
                        void *           app_ctx,
                        rnp_key_handle_t key,
                        const char *     pgp_context,
                        char             buf[],
                        size_t           buf_len)
{
    char *     keyid = NULL;
    char       target[64] = {0};
    char       prompt[128] = {0};
    char *     buffer = NULL;
    bool       ok = false;
    bool       protect = false;
    bool       add_subkey = false;
    bool       decrypt_symmetric = false;
    bool       encrypt_symmetric = false;
    bool       is_primary = false;
    cli_rnp_t *rnp = static_cast<cli_rnp_t *>(app_ctx);

    if (!ffi || !pgp_context) {
        goto done;
    }

    if (!strcmp(pgp_context, "protect")) {
        protect = true;
    } else if (!strcmp(pgp_context, "add subkey")) {
        add_subkey = true;
    } else if (!strcmp(pgp_context, "decrypt (symmetric)")) {
        decrypt_symmetric = true;
    } else if (!strcmp(pgp_context, "encrypt (symmetric)")) {
        encrypt_symmetric = true;
    }

    if (!decrypt_symmetric && !encrypt_symmetric) {
        rnp_key_get_keyid(key, &keyid);
        snprintf(target, sizeof(target), "key 0x%s", keyid);
        rnp_buffer_destroy(keyid);
        (void) rnp_key_is_primary(key, &is_primary);
    }

    if ((protect || add_subkey) && rnp->reuse_password_for_subkey && !is_primary) {
        char *primary_fprint = NULL;
        if (rnp_key_get_primary_fprint(key, &primary_fprint) == RNP_SUCCESS &&
            !rnp->reuse_primary_fprint.empty() &&
            rnp->reuse_primary_fprint == primary_fprint) {
            strncpy(buf, rnp->reused_password, buf_len);
            ok = true;
        }

        rnp_buffer_clear(rnp->reused_password, strnlen(rnp->reused_password, buf_len));
        free(rnp->reused_password);
        rnp->reused_password = NULL;
        rnp->reuse_password_for_subkey = false;
        rnp_buffer_destroy(primary_fprint);
        if (ok)
            return true;
    }

    buffer = (char *) calloc(1, buf_len);
    if (!buffer) {
        return false;
    }
start:
    if (decrypt_symmetric) {
        snprintf(prompt, sizeof(prompt), "Enter password to decrypt data: ");
    } else if (encrypt_symmetric) {
        snprintf(prompt, sizeof(prompt), "Enter password to encrypt data: ");
    } else {
        snprintf(prompt, sizeof(prompt), "Enter password for %s to %s: ", target, pgp_context);
    }

    if (!stdin_getpass(prompt, buf, buf_len, *rnp)) {
        goto done;
    }
    if (protect || encrypt_symmetric) {
        if (protect) {
            snprintf(prompt, sizeof(prompt), "Repeat password for %s: ", target);
        } else {
            snprintf(prompt, sizeof(prompt), "Repeat password: ");
        }

        if (!stdin_getpass(prompt, buffer, buf_len, *rnp)) {
            goto done;
        }
        if (strcmp(buf, buffer) != 0) {
            fputs("\nPasswords do not match!", rnp->userio_out);
            // currently will loop forever
            goto start;
        }
        if (strnlen(buf, buf_len) == 0 && !rnp->cfg().get_bool(CFG_FORCE)) {
            if (!cli_rnp_get_confirmation(
                  rnp, "Password is empty. The key will be left unprotected. Are you sure?")) {
                goto start;
            }
        }
    }
    if ((protect || add_subkey) && is_primary) {
        if (cli_rnp_get_confirmation(
              rnp, "Would you like to use the same password to protect subkey(s)?")) {
            char *primary_fprint = NULL;
            rnp->reuse_password_for_subkey = true;
            rnp_key_get_fprint(key, &primary_fprint);
            rnp->reuse_primary_fprint = primary_fprint;
            rnp->reused_password = strdup(buf);
            rnp_buffer_destroy(primary_fprint);
        }
    }
    ok = true;
done:
    fputs("", rnp->userio_out);
    rnp_buffer_clear(buffer, buf_len);
    free(buffer);
    return ok;
}

static bool
ffi_pass_callback_file(rnp_ffi_t        ffi,
                       void *           app_ctx,
                       rnp_key_handle_t key,
                       const char *     pgp_context,
                       char             buf[],
                       size_t           buf_len)
{
    if (!app_ctx || !buf || !buf_len) {
        return false;
    }

    FILE *fp = (FILE *) app_ctx;
    if (!fgets(buf, buf_len, fp)) {
        return false;
    }
    rnp::strip_eol(buf);
    return true;
}

static bool
ffi_pass_callback_string(rnp_ffi_t        ffi,
                         void *           app_ctx,
                         rnp_key_handle_t key,
                         const char *     pgp_context,
                         char             buf[],
                         size_t           buf_len)
{
    if (!app_ctx || !buf || !buf_len) {
        return false;
    }

    const char *pswd = (const char *) app_ctx;
    if (strlen(pswd) >= buf_len) {
        return false;
    }

    strncpy(buf, pswd, buf_len);
    return true;
}

static void
ffi_key_callback(rnp_ffi_t   ffi,
                 void *      app_ctx,
                 const char *identifier_type,
                 const char *identifier,
                 bool        secret)
{
    cli_rnp_t *rnp = static_cast<cli_rnp_t *>(app_ctx);

    if (rnp::str_case_eq(identifier_type, "keyid") &&
        rnp::str_case_eq(identifier, "0000000000000000")) {
        if (rnp->hidden_msg) {
            return;
        }
        ERR_MSG("This message has hidden recipient. Will attempt to use all secret keys for "
                "decryption.");
        rnp->hidden_msg = true;
    }
}

#ifdef _WIN32
void
rnpffiInvalidParameterHandler(const wchar_t *expression,
                              const wchar_t *function,
                              const wchar_t *file,
                              unsigned int   line,
                              uintptr_t      pReserved)
{
    // do nothing as within release CRT all params are NULL
}
#endif

cli_rnp_t::~cli_rnp_t()
{
    end();
#ifdef _WIN32
    if (subst_argv) {
        rnp_win_clear_args(subst_argc, subst_argv);
    }
#endif
}

int
cli_rnp_t::ret_code(bool success)
{
    return success ? EXIT_SUCCESS : EXIT_FAILURE;
}

#ifdef _WIN32
void
cli_rnp_t::substitute_args(int *argc, char ***argv)
{
    rnp_win_substitute_cmdline_args(argc, argv);
    subst_argc = *argc;
    subst_argv = *argv;
}
#endif

bool
cli_rnp_t::init(const rnp_cfg &cfg)
{
    cfg_.copy(cfg);

    /* Configure user's io streams. */
    if (!cfg_.get_bool(CFG_NOTTY)) {
        userio_in = (isatty(fileno(stdin)) ? stdin : fopen("/dev/tty", "r"));
        userio_in = (userio_in ? userio_in : stdin);
        userio_out = (isatty(fileno(stdout)) ? stdout : fopen("/dev/tty", "a+"));
        userio_out = (userio_out ? userio_out : stdout);
    } else {
        userio_in = stdin;
        userio_out = stdout;
    }

#ifndef _WIN32
    /* If system resource constraints are in effect then attempt to
     * disable core dumps.
     */
    bool coredumps = true;
    if (!cfg_.get_bool(CFG_COREDUMPS)) {
#ifdef HAVE_SYS_RESOURCE_H
        coredumps = !disable_core_dumps();
#endif
    }

    if (coredumps) {
        ERR_MSG("warning: core dumps may be enabled, sensitive data may be leaked to disk");
    }
#endif

#ifdef _WIN32
    /* Setup invalid parameter handler for Windows */
    _invalid_parameter_handler handler = rnpffiInvalidParameterHandler;
    _set_invalid_parameter_handler(handler);
    _CrtSetReportMode(_CRT_ASSERT, 0);
#endif

    /* Configure the results stream. */
    // TODO: UTF8?
    const std::string &ress = cfg_.get_str(CFG_IO_RESS);
    if (ress.empty() || (ress == "<stderr>")) {
        resfp = stderr;
    } else if (ress == "<stdout>") {
        resfp = stdout;
    } else if (!(resfp = rnp_fopen(ress.c_str(), "w"))) {
        ERR_MSG("Cannot open results %s for writing", ress.c_str());
        return false;
    }

    bool              res = false;
    const std::string pformat = pubformat();
    const std::string sformat = secformat();
    if (pformat.empty() || sformat.empty()) {
        ERR_MSG("Unknown public or secret keyring format");
        return false;
    }
    if (rnp_ffi_create(&ffi, pformat.c_str(), sformat.c_str())) {
        ERR_MSG("Failed to initialize FFI");
        return false;
    }

    // by default use stdin password provider
    if (rnp_ffi_set_pass_provider(ffi, ffi_pass_callback_stdin, this)) {
        goto done;
    }

    // set key provider, currently for informational purposes only
    if (rnp_ffi_set_key_provider(ffi, ffi_key_callback, this)) {
        goto done;
    }

    // setup file/pipe password input if requested
    if (cfg_.get_int(CFG_PASSFD, -1) >= 0) {
        if (!set_pass_fd(&passfp, cfg_.get_int(CFG_PASSFD))) {
            goto done;
        }
        if (rnp_ffi_set_pass_provider(ffi, ffi_pass_callback_file, passfp)) {
            goto done;
        }
    }
    // setup current time if requested
    if (cfg_.has(CFG_CURTIME)) {
        rnp_set_timestamp(ffi, cfg_.time());
    }
    pswdtries = MAX_PASSWORD_ATTEMPTS;
    res = true;
done:
    if (!res) {
        rnp_ffi_destroy(ffi);
        ffi = NULL;
    }
    return res;
}

void
cli_rnp_t::end()
{
    if (passfp) {
        fclose(passfp);
        passfp = NULL;
    }
    if (resfp && (resfp != stderr) && (resfp != stdout)) {
        fclose(resfp);
        resfp = NULL;
    }
    if (userio_in && userio_in != stdin) {
        fclose(userio_in);
    }
    userio_in = NULL;
    if (userio_out && userio_out != stdout) {
        fclose(userio_out);
    }
    userio_out = NULL;
    rnp_ffi_destroy(ffi);
    ffi = NULL;
    cfg_.clear();
    reuse_primary_fprint.clear();
    if (reused_password) {
        rnp_buffer_clear(reused_password, strlen(reused_password));
        free(reused_password);
        reused_password = NULL;
    }
    reuse_password_for_subkey = false;
}

bool
cli_rnp_t::load_keyring(bool secret)
{
    const std::string &path = secret ? secpath() : pubpath();
    bool               dir = secret && (secformat() == RNP_KEYSTORE_G10);
    if (!rnp::path::exists(path, dir)) {
        return true;
    }

    rnp_input_t keyin = NULL;
    if (rnp_input_from_path(&keyin, path.c_str())) {
        ERR_MSG("Warning: failed to open keyring at path '%s' for reading.", path.c_str());
        return true;
    }

    const char * format = secret ? secformat().c_str() : pubformat().c_str();
    uint32_t     flags = secret ? RNP_LOAD_SAVE_SECRET_KEYS : RNP_LOAD_SAVE_PUBLIC_KEYS;
    rnp_result_t ret = rnp_load_keys(ffi, format, keyin, flags);
    if (ret) {
        ERR_MSG("Error: failed to load keyring from '%s'", path.c_str());
    }
    rnp_input_destroy(keyin);

    if (ret) {
        return false;
    }

    size_t keycount = 0;
    if (secret) {
        (void) rnp_get_secret_key_count(ffi, &keycount);
    } else {
        (void) rnp_get_public_key_count(ffi, &keycount);
    }
    if (!keycount) {
        ERR_MSG("Warning: no keys were loaded from the keyring '%s'.", path.c_str());
    }
    return true;
}

bool
cli_rnp_t::load_keyrings(bool loadsecret)
{
    /* Read public keys */
    if (rnp_unload_keys(ffi, RNP_KEY_UNLOAD_PUBLIC)) {
        ERR_MSG("failed to clear public keyring");
        return false;
    }

    if (!load_keyring(false)) {
        return false;
    }

    /* Only read secret keys if we need to */
    if (loadsecret) {
        if (rnp_unload_keys(ffi, RNP_KEY_UNLOAD_SECRET)) {
            ERR_MSG("failed to clear secret keyring");
            return false;
        }

        if (!load_keyring(true)) {
            return false;
        }
    }
    if (defkey().empty()) {
        set_defkey();
    }
    return true;
}

void
cli_rnp_t::set_defkey()
{
    rnp_identifier_iterator_t it = NULL;
    rnp_key_handle_t          handle = NULL;
    const char *              grip = NULL;

    cfg_.unset(CFG_KR_DEF_KEY);
    if (rnp_identifier_iterator_create(ffi, &it, "grip")) {
        ERR_MSG("failed to create key iterator");
        return;
    }

    while (!rnp_identifier_iterator_next(it, &grip)) {
        bool is_subkey = false;
        bool is_secret = false;

        if (!grip) {
            break;
        }
        if (rnp_locate_key(ffi, "grip", grip, &handle)) {
            ERR_MSG("failed to locate key");
            continue;
        }
        if (rnp_key_is_sub(handle, &is_subkey) || is_subkey) {
            goto next;
        }
        if (rnp_key_have_secret(handle, &is_secret)) {
            goto next;
        }
        if (!cfg_.has(CFG_KR_DEF_KEY) || is_secret) {
            cfg_.set_str(CFG_KR_DEF_KEY, grip);
            /* if we have secret primary key then use it as default */
            if (is_secret) {
                break;
            }
        }
    next:
        rnp_key_handle_destroy(handle);
        handle = NULL;
    }
    rnp_key_handle_destroy(handle);
    rnp_identifier_iterator_destroy(it);
}

bool
cli_rnp_t::is_cv25519_subkey(rnp_key_handle_t handle)
{
    bool primary = false;
    if (rnp_key_is_primary(handle, &primary)) {
        ERR_MSG("Error: failed to check for subkey.");
        return false;
    }
    if (primary) {
        return false;
    }
    char *alg = NULL;
    if (rnp_key_get_alg(handle, &alg)) {
        ERR_MSG("Error: failed to check key's alg.");
        return false;
    }
    bool ecdh = !strcmp(alg, RNP_ALGNAME_ECDH);
    rnp_buffer_destroy(alg);
    if (!ecdh) {
        return false;
    }
    char *curve = NULL;
    if (rnp_key_get_curve(handle, &curve)) {
        ERR_MSG("Error: failed to check key's curve.");
        return false;
    }
    bool cv25519 = !strcmp(curve, "Curve25519");
    rnp_buffer_destroy(curve);
    return cv25519;
}

bool
cli_rnp_t::get_protection(rnp_key_handle_t handle,
                          std::string &    hash,
                          std::string &    cipher,
                          size_t &         iterations)
{
    bool prot = false;
    if (rnp_key_is_protected(handle, &prot)) {
        ERR_MSG("Error: failed to check key's protection.");
        return false;
    }
    if (!prot) {
        hash = "";
        cipher = "";
        iterations = 0;
        return true;
    }

    char *val = NULL;
    try {
        if (rnp_key_get_protection_hash(handle, &val)) {
            ERR_MSG("Error: failed to retrieve key's protection hash.");
            return false;
        }
        hash = val;
        rnp_buffer_destroy(val);
        if (rnp_key_get_protection_cipher(handle, &val)) {
            ERR_MSG("Error: failed to retrieve key's protection cipher.");
            return false;
        }
        cipher = val;
        rnp_buffer_destroy(val);
        if (rnp_key_get_protection_iterations(handle, &iterations)) {
            ERR_MSG("Error: failed to retrieve key's protection iterations.");
            return false;
        }
        return true;
    } catch (const std::exception &e) {
        ERR_MSG("Error: failed to retrieve key's properties: %s", e.what());
        rnp_buffer_destroy(val);
        return false;
    }
}

bool
cli_rnp_t::check_cv25519_bits(rnp_key_handle_t key, char *prot_password, bool &tweaked)
{
    /* unlock key first to check whether bits are tweaked */
    if (prot_password && rnp_key_unlock(key, prot_password)) {
        ERR_MSG("Error: failed to unlock key. Did you specify valid password?");
        return false;
    }
    rnp_result_t ret = rnp_key_25519_bits_tweaked(key, &tweaked);
    if (ret) {
        ERR_MSG("Error: failed to check whether key's bits are tweaked.");
    }
    if (prot_password) {
        rnp_key_lock(key);
    }
    return !ret;
}

bool
cli_rnp_t::fix_cv25519_subkey(const std::string &key, bool checkonly)
{
    std::vector<rnp_key_handle_t> keys;
    if (!cli_rnp_keys_matching_string(
          this, keys, key, CLI_SEARCH_SECRET | CLI_SEARCH_SUBKEYS)) {
        ERR_MSG("Secret keys matching '%s' not found.", key.c_str());
        return false;
    }
    bool        res = false;
    std::string prot_hash;
    std::string prot_cipher;
    size_t      prot_iterations;
    char *      prot_password = NULL;
    bool        tweaked = false;

    if (keys.size() > 1) {
        ERR_MSG(
          "Ambiguous input: too many keys found for '%s'. Did you use keyid or fingerprint?",
          key.c_str());
        goto done;
    }
    cli_rnp_print_key_info(userio_out, ffi, keys[0], true, false);
    if (!is_cv25519_subkey(keys[0])) {
        ERR_MSG("Error: specified key is not Curve25519 ECDH subkey.");
        goto done;
    }

    if (!get_protection(keys[0], prot_hash, prot_cipher, prot_iterations)) {
        goto done;
    }

    if (!prot_hash.empty() &&
        (rnp_request_password(ffi, keys[0], "unprotect", &prot_password) || !prot_password)) {
        ERR_MSG("Error: failed to obtain protection password.");
        goto done;
    }

    if (!check_cv25519_bits(keys[0], prot_password, tweaked)) {
        goto done;
    }

    if (checkonly) {
        fprintf(userio_out,
                tweaked ? "Cv25519 key bits are set correctly and do not require fixing.\n" :
                          "Warning: Cv25519 key bits need fixing.\n");
        res = tweaked;
        goto done;
    }

    if (tweaked) {
        ERR_MSG("Warning: key's bits are fixed already, no action is required.");
        res = true;
        goto done;
    }

    /* now unprotect so we can tweak bits */
    if (!prot_hash.empty()) {
        if (rnp_key_unprotect(keys[0], prot_password)) {
            ERR_MSG("Error: failed to unprotect key. Did you specify valid password?");
            goto done;
        }
        if (rnp_key_unlock(keys[0], NULL)) {
            ERR_MSG("Error: failed to unlock key.");
            goto done;
        }
    }

    /* tweak key bits and protect back */
    if (rnp_key_25519_bits_tweak(keys[0])) {
        ERR_MSG("Error: failed to tweak key's bits.");
        goto done;
    }

    if (!prot_hash.empty() && rnp_key_protect(keys[0],
                                              prot_password,
                                              prot_cipher.c_str(),
                                              NULL,
                                              prot_hash.c_str(),
                                              prot_iterations)) {
        ERR_MSG("Error: failed to protect key back.");
        goto done;
    }

    res = cli_rnp_save_keyrings(this);
done:
    clear_key_handles(keys);
    if (prot_password) {
        rnp_buffer_clear(prot_password, strlen(prot_password) + 1);
        rnp_buffer_destroy(prot_password);
    }
    return res;
}

bool
cli_rnp_t::set_key_expire(const std::string &key)
{
    std::vector<rnp_key_handle_t> keys;
    if (!cli_rnp_keys_matching_string(
          this, keys, key, CLI_SEARCH_SECRET | CLI_SEARCH_SUBKEYS)) {
        ERR_MSG("Secret keys matching '%s' not found.", key.c_str());
        return false;
    }
    bool     res = false;
    uint32_t expiration = 0;
    if (keys.size() > 1) {
        ERR_MSG("Ambiguous input: too many keys found for '%s'.", key.c_str());
        goto done;
    }
    if (!cfg().get_expiration(CFG_SET_KEY_EXPIRE, expiration) ||
        rnp_key_set_expiration(keys[0], expiration)) {
        ERR_MSG("Failed to set key expiration.");
        goto done;
    }
    res = cli_rnp_save_keyrings(this);
done:
    if (res) {
        cli_rnp_print_key_info(stdout, ffi, keys[0], true, false);
    }
    clear_key_handles(keys);
    return res;
}

bool
cli_rnp_t::add_new_subkey(const std::string &key)
{
    rnp_cfg &lcfg = cfg();
    if (!cli_rnp_set_generate_params(lcfg, true)) {
        ERR_MSG("Subkey generation setup failed.");
        return false;
    }
    std::vector<rnp_key_handle_t> keys;
    if (!cli_rnp_keys_matching_string(this, keys, key, CLI_SEARCH_SECRET)) {
        ERR_MSG("Secret keys matching '%s' not found.", key.c_str());
        return false;
    }
    bool              res = false;
    rnp_op_generate_t genkey = NULL;
    rnp_key_handle_t  subkey = NULL;
    char *            password = NULL;

    if (keys.size() > 1) {
        ERR_MSG("Ambiguous input: too many keys found for '%s'.", key.c_str());
        goto done;
    }
    if (rnp_op_generate_subkey_create(
          &genkey, ffi, keys[0], cfg().get_cstr(CFG_KG_SUBKEY_ALG))) {
        ERR_MSG("Failed to initialize subkey generation.");
        goto done;
    }
    if (cfg().has(CFG_KG_SUBKEY_BITS) &&
        rnp_op_generate_set_bits(genkey, cfg().get_int(CFG_KG_SUBKEY_BITS))) {
        ERR_MSG("Failed to set subkey bits.");
        goto done;
    }
    if (cfg().has(CFG_KG_SUBKEY_CURVE) &&
        rnp_op_generate_set_curve(genkey, cfg().get_cstr(CFG_KG_SUBKEY_CURVE))) {
        ERR_MSG("Failed to set subkey curve.");
        goto done;
    }
    if (cfg().has(CFG_KG_SUBKEY_EXPIRATION)) {
        uint32_t expiration = 0;
        if (!cfg().get_expiration(CFG_KG_SUBKEY_EXPIRATION, expiration) ||
            rnp_op_generate_set_expiration(genkey, expiration)) {
            ERR_MSG("Failed to set subkey expiration.");
            goto done;
        }
    }
    // TODO : set DSA qbits
    if (rnp_op_generate_set_hash(genkey, cfg().get_cstr(CFG_KG_HASH))) {
        ERR_MSG("Failed to set hash algorithm.");
        goto done;
    }
    if (rnp_op_generate_execute(genkey) || rnp_op_generate_get_key(genkey, &subkey)) {
        ERR_MSG("Subkey generation failed.");
        goto done;
    }
    if (rnp_request_password(ffi, subkey, "protect", &password)) {
        ERR_MSG("Failed to obtain protection password.");
        goto done;
    }
    if (*password) {
        rnp_result_t ret = rnp_key_protect(subkey,
                                           password,
                                           cfg().get_cstr(CFG_KG_PROT_ALG),
                                           NULL,
                                           cfg().get_cstr(CFG_KG_PROT_HASH),
                                           cfg().get_int(CFG_KG_PROT_ITERATIONS));
        rnp_buffer_clear(password, strlen(password) + 1);
        rnp_buffer_destroy(password);
        if (ret) {
            ERR_MSG("Failed to protect key.");
            goto done;
        }
    } else {
        rnp_buffer_destroy(password);
    }
    res = cli_rnp_save_keyrings(this);
done:
    if (res) {
        cli_rnp_print_key_info(stdout, ffi, keys[0], true, false);
        if (subkey) {
            cli_rnp_print_key_info(stdout, ffi, subkey, true, false);
        }
    }
    clear_key_handles(keys);
    rnp_op_generate_destroy(genkey);
    rnp_key_handle_destroy(subkey);
    return res;
}

bool
cli_rnp_t::edit_key(const std::string &key)
{
    int edit_options = 0;

    if (cfg().get_bool(CFG_CHK_25519_BITS)) {
        edit_options++;
    }
    if (cfg().get_bool(CFG_FIX_25519_BITS)) {
        edit_options++;
    }
    if (cfg().get_bool(CFG_ADD_SUBKEY)) {
        edit_options++;
    }
    if (cfg().has(CFG_SET_KEY_EXPIRE)) {
        edit_options++;
    }

    if (!edit_options) {
        ERR_MSG("You should specify one of the editing options for --edit-key.");
        return false;
    }
    if (edit_options > 1) {
        ERR_MSG("Only one key edit option can be executed at a time.");
        return false;
    }

    if (cfg().get_bool(CFG_CHK_25519_BITS)) {
        return fix_cv25519_subkey(key, true);
    }
    if (cfg().get_bool(CFG_FIX_25519_BITS)) {
        return fix_cv25519_subkey(key, false);
    }

    if (cfg().get_bool(CFG_ADD_SUBKEY)) {
        return add_new_subkey(key);
    }

    if (cfg().has(CFG_SET_KEY_EXPIRE)) {
        return set_key_expire(key);
    }

    return false;
}

const char *
json_obj_get_str(json_object *obj, const char *key)
{
    json_object *fld = NULL;
    if (!json_object_object_get_ex(obj, key, &fld)) {
        return NULL;
    }
    return json_object_get_string(fld);
}

static char *
cli_key_usage_str(rnp_key_handle_t key, char *buf)
{
    char *orig = buf;
    bool  allow = false;

    if (!rnp_key_allows_usage(key, "encrypt", &allow) && allow) {
        *buf++ = 'E';
    }
    allow = false;
    if (!rnp_key_allows_usage(key, "sign", &allow) && allow) {
        *buf++ = 'S';
    }
    allow = false;
    if (!rnp_key_allows_usage(key, "certify", &allow) && allow) {
        *buf++ = 'C';
    }
    allow = false;
    if (!rnp_key_allows_usage(key, "authenticate", &allow) && allow) {
        *buf++ = 'A';
    }
    *buf = '\0';
    return orig;
}

std::string
cli_rnp_escape_string(const std::string &src)
{
    static const int   SPECIAL_CHARS_COUNT = 0x20;
    static const char *escape_map[SPECIAL_CHARS_COUNT + 1] = {
      "\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\x07",
      "\\b",   "\\x09", "\\n",   "\\v",   "\\f",   "\\r",   "\\x0e", "\\x0f",
      "\\x10", "\\x11", "\\x12", "\\x13", "\\x14", "\\x15", "\\x16", "\\x17",
      "\\x18", "\\x19", "\\x1a", "\\x1b", "\\x1c", "\\x1d", "\\x1e", "\\x1f",
      "\\x20" // space should not be auto-replaced
    };
    std::string result;
    // we want to replace leading and trailing spaces with escape codes to make them visible
    auto        original_len = src.length();
    std::string rtrimmed = src;
    bool        leading_space = true;
    rtrimmed.erase(rtrimmed.find_last_not_of(0x20) + 1);
    result.reserve(original_len);
    for (char const &c : rtrimmed) {
        leading_space &= c == 0x20;
        if (leading_space || (c >= 0 && c < SPECIAL_CHARS_COUNT)) {
            result.append(escape_map[(int) c]);
        } else {
            result.push_back(c);
        }
    }
    // printing trailing spaces
    for (auto pos = rtrimmed.length(); pos < original_len; pos++) {
        result.append(escape_map[0x20]);
    }
    return result;
}

static const std::string alg_aliases[] = {
  "3DES",         "TRIPLEDES",   "3-DES",        "TRIPLEDES",   "CAST-5",       "CAST5",
  "AES",          "AES128",      "AES-128",      "AES128",      "AES-192",      "AES192",
  "AES-256",      "AES256",      "CAMELLIA-128", "CAMELLIA128", "CAMELLIA-192", "CAMELLIA192",
  "CAMELLIA-256", "CAMELLIA256", "SHA",          "SHA1",        "SHA-1",        "SHA1",
  "SHA-224",      "SHA224",      "SHA-256",      "SHA256",      "SHA-384",      "SHA384",
  "SHA-512",      "SHA512",      "RIPEMD-160",   "RIPEMD160"};

const std::string
cli_rnp_alg_to_ffi(const std::string alg)
{
    size_t count = sizeof(alg_aliases) / sizeof(alg_aliases[0]);
    assert((count % 2) == 0);
    for (size_t idx = 0; idx < count; idx += 2) {
        if (rnp::str_case_eq(alg, alg_aliases[idx])) {
            return alg_aliases[idx + 1];
        }
    }
    return alg;
}

bool
cli_rnp_set_hash(rnp_cfg &cfg, const std::string &hash)
{
    bool  supported = false;
    auto &alg = cli_rnp_alg_to_ffi(hash);
    if (rnp_supports_feature(RNP_FEATURE_HASH_ALG, alg.c_str(), &supported) || !supported) {
        ERR_MSG("Unsupported hash algorithm: %s", hash.c_str());
        return false;
    }
    cfg.set_str(CFG_HASH, alg);
    return true;
}

bool
cli_rnp_set_cipher(rnp_cfg &cfg, const std::string &cipher)
{
    bool  supported = false;
    auto &alg = cli_rnp_alg_to_ffi(cipher);
    if (rnp_supports_feature(RNP_FEATURE_SYMM_ALG, alg.c_str(), &supported) || !supported) {
        ERR_MSG("Unsupported encryption algorithm: %s", cipher.c_str());
        return false;
    }
    cfg.set_str(CFG_CIPHER, alg);
    return true;
}

#ifndef RNP_USE_STD_REGEX
static std::string
cli_rnp_unescape_for_regcomp(const std::string &src)
{
    std::string result;
    result.reserve(src.length());
    regex_t    r = {};
    regmatch_t matches[1];
    if (regcomp(&r, "\\\\x[0-9a-f]([0-9a-f])?", REG_EXTENDED | REG_ICASE) != 0)
        return src;

    int offset = 0;
    while (regexec(&r, src.c_str() + offset, 1, matches, 0) == 0) {
        result.append(src, offset, matches[0].rm_so);
        int         hexoff = matches[0].rm_so + 2;
        std::string hex;
        hex.push_back(src[offset + hexoff]);
        if (hexoff + 1 < matches[0].rm_eo) {
            hex.push_back(src[offset + hexoff + 1]);
        }
        char decoded = stoi(hex, 0, 16);
        if ((decoded >= 0x7B && decoded <= 0x7D) || (decoded >= 0x24 && decoded <= 0x2E) ||
            decoded == 0x5C || decoded == 0x5E) {
            result.push_back('\\');
            result.push_back(decoded);
        } else if ((decoded == '[' || decoded == ']') &&
                   /* not enclosed in [] */ (result.empty() || result.back() != '[')) {
            result.push_back('[');
            result.push_back(decoded);
            result.push_back(']');
        } else {
            result.push_back(decoded);
        }
        offset += matches[0].rm_eo;
    }

    result.append(src.begin() + offset, src.end());

    return result;
}
#endif

/* Convert key algorithm constant to one displayed to the user */
static const char *
cli_rnp_normalize_key_alg(const char *alg)
{
    if (!strcmp(alg, RNP_ALGNAME_EDDSA)) {
        return "EdDSA";
    }
    if (!strcmp(alg, RNP_ALGNAME_ELGAMAL)) {
        return "ElGamal";
    }
    return alg;
}

static void
cli_rnp_print_sig_info(FILE *fp, rnp_ffi_t ffi, rnp_signature_handle_t sig)
{
    uint32_t creation = 0;
    (void) rnp_signature_get_creation(sig, &creation);

    char *keyfp = NULL;
    char *keyid = NULL;
    (void) rnp_signature_get_key_fprint(sig, &keyfp);
    (void) rnp_signature_get_keyid(sig, &keyid);

    char *           signer_uid = NULL;
    rnp_key_handle_t signer = NULL;
    if (keyfp) {
        /* Fingerprint lookup is faster */
        (void) rnp_locate_key(ffi, "fingerprint", keyfp, &signer);
    } else if (keyid) {
        (void) rnp_locate_key(ffi, "keyid", keyid, &signer);
    }
    if (signer) {
        /* signer primary uid */
        (void) rnp_key_get_primary_uid(signer, &signer_uid);
    }

    /* signer key id */
    fprintf(fp, "sig           %s ", keyid ? rnp::lowercase(keyid) : "[no key id]");
    /* signature creation time */
    char buf[64] = {0};
    fprintf(fp, "%s", ptimestr(buf, sizeof(buf), creation));
    /* signer's userid */
    fprintf(fp, " %s", signer_uid ? signer_uid : "[unknown]");
    /* signature validity */
    const char * valmsg = NULL;
    rnp_result_t validity = rnp_signature_is_valid(sig, 0);
    switch (validity) {
    case RNP_SUCCESS:
        valmsg = "";
        break;
    case RNP_ERROR_SIGNATURE_EXPIRED:
        valmsg = " [expired]";
        break;
    case RNP_ERROR_SIGNATURE_INVALID:
        valmsg = " [invalid]";
        break;
    default:
        valmsg = " [unverified]";
    }
    fprintf(fp, "%s\n", valmsg);

    (void) rnp_key_handle_destroy(signer);
    rnp_buffer_destroy(keyid);
    rnp_buffer_destroy(keyfp);
    rnp_buffer_destroy(signer_uid);
}

void
cli_rnp_print_key_info(FILE *fp, rnp_ffi_t ffi, rnp_key_handle_t key, bool psecret, bool psigs)
{
    char        buf[64] = {0};
    const char *header = NULL;
    bool        secret = false;
    bool        primary = false;

    /* header */
    if (rnp_key_have_secret(key, &secret) || rnp_key_is_primary(key, &primary)) {
        fprintf(fp, "Key error.\n");
        return;
    }

    if (psecret && secret) {
        header = primary ? "sec" : "ssb";
    } else {
        header = primary ? "pub" : "sub";
    }
    if (primary) {
        fprintf(fp, "\n");
    }
    fprintf(fp, "%s   ", header);

    /* key bits */
    uint32_t bits = 0;
    rnp_key_get_bits(key, &bits);
    fprintf(fp, "%d/", (int) bits);
    /* key algorithm */
    char *alg = NULL;
    (void) rnp_key_get_alg(key, &alg);
    fprintf(fp, "%s ", cli_rnp_normalize_key_alg(alg));
    /* key id */
    char *keyid = NULL;
    (void) rnp_key_get_keyid(key, &keyid);
    fprintf(fp, "%s", rnp::lowercase(keyid));
    /* key creation time */
    uint32_t create = 0;
    (void) rnp_key_get_creation(key, &create);
    fprintf(fp, " %s", ptimestr(buf, sizeof(buf), create));
    /* key usage */
    bool valid = false;
    bool expired = false;
    bool revoked = false;
    (void) rnp_key_is_valid(key, &valid);
    (void) rnp_key_is_expired(key, &expired);
    (void) rnp_key_is_revoked(key, &revoked);
    if (valid || expired || revoked) {
        fprintf(fp, " [%s]", cli_key_usage_str(key, buf));
    } else {
        fprintf(fp, " [INVALID]");
    }
    /* key expiration */
    uint32_t expiry = 0;
    (void) rnp_key_get_expiration(key, &expiry);
    if (expiry) {
        ptimestr(buf, sizeof(buf), create + expiry);
        fprintf(fp, " [%s %s]", expired ? "EXPIRED" : "EXPIRES", buf);
    }
    /* key is revoked */
    if (revoked) {
        fprintf(fp, " [REVOKED]");
    }
    /* fingerprint */
    char *keyfp = NULL;
    (void) rnp_key_get_fprint(key, &keyfp);
    fprintf(fp, "\n      %s\n", rnp::lowercase(keyfp));
    /* direct-key or binding signatures */
    if (psigs) {
        size_t sigs = 0;
        (void) rnp_key_get_signature_count(key, &sigs);
        for (size_t i = 0; i < sigs; i++) {
            rnp_signature_handle_t sig = NULL;
            (void) rnp_key_get_signature_at(key, i, &sig);
            if (!sig) {
                continue;
            }
            cli_rnp_print_sig_info(fp, ffi, sig);
            rnp_signature_handle_destroy(sig);
        }
    }
    /* user ids */
    size_t uids = 0;
    (void) rnp_key_get_uid_count(key, &uids);
    for (size_t i = 0; i < uids; i++) {
        rnp_uid_handle_t uid = NULL;

        if (rnp_key_get_uid_handle_at(key, i, &uid)) {
            continue;
        }
        bool  revoked = false;
        bool  valid = false;
        char *uid_str = NULL;
        (void) rnp_uid_is_revoked(uid, &revoked);
        (void) rnp_uid_is_valid(uid, &valid);
        (void) rnp_key_get_uid_at(key, i, &uid_str);

        /* userid itself with revocation status */
        fprintf(fp, "uid           %s", cli_rnp_escape_string(uid_str).c_str());
        fprintf(fp, "%s\n", revoked ? " [REVOKED]" : valid ? "" : " [INVALID]");
        rnp_buffer_destroy(uid_str);

        /* print signatures only if requested */
        if (!psigs) {
            (void) rnp_uid_handle_destroy(uid);
            continue;
        }

        size_t sigs = 0;
        (void) rnp_uid_get_signature_count(uid, &sigs);
        for (size_t j = 0; j < sigs; j++) {
            rnp_signature_handle_t sig = NULL;
            (void) rnp_uid_get_signature_at(uid, j, &sig);
            if (!sig) {
                continue;
            }
            cli_rnp_print_sig_info(fp, ffi, sig);
            rnp_signature_handle_destroy(sig);
        }
        (void) rnp_uid_handle_destroy(uid);
    }

    rnp_buffer_destroy(alg);
    rnp_buffer_destroy(keyid);
    rnp_buffer_destroy(keyfp);
}

bool
cli_rnp_save_keyrings(cli_rnp_t *rnp)
{
    rnp_output_t       output = NULL;
    rnp_result_t       pub_ret = 0;
    rnp_result_t       sec_ret = 0;
    const std::string &ppath = rnp->pubpath();
    const std::string &spath = rnp->secpath();

    // check whether we have G10 secret keyring - then need to create directory
    if (rnp->secformat() == "G10") {
        struct stat path_stat;
        if (rnp_stat(spath.c_str(), &path_stat) != -1) {
            if (!S_ISDIR(path_stat.st_mode)) {
                ERR_MSG("G10 keystore should be a directory: %s", spath.c_str());
                return false;
            }
        } else {
            if (errno != ENOENT) {
                ERR_MSG("stat(%s): %s", spath.c_str(), strerror(errno));
                return false;
            }
            if (RNP_MKDIR(spath.c_str(), S_IRWXU) != 0) {
                ERR_MSG("mkdir(%s, S_IRWXU): %s", spath.c_str(), strerror(errno));
                return false;
            }
        }
    }

    // public keyring
    if (!(pub_ret = rnp_output_to_path(&output, ppath.c_str()))) {
        pub_ret =
          rnp_save_keys(rnp->ffi, rnp->pubformat().c_str(), output, RNP_LOAD_SAVE_PUBLIC_KEYS);
        rnp_output_destroy(output);
    }
    if (pub_ret) {
        ERR_MSG("failed to write pubring to path '%s'", ppath.c_str());
    }

    // secret keyring
    if (!(sec_ret = rnp_output_to_path(&output, spath.c_str()))) {
        sec_ret =
          rnp_save_keys(rnp->ffi, rnp->secformat().c_str(), output, RNP_LOAD_SAVE_SECRET_KEYS);
        rnp_output_destroy(output);
    }
    if (sec_ret) {
        ERR_MSG("failed to write secring to path '%s'", spath.c_str());
    }

    return !pub_ret && !sec_ret;
}

bool
cli_rnp_generate_key(cli_rnp_t *rnp, const char *username)
{
    /* set key generation parameters to rnp_cfg_t */
    rnp_cfg &cfg = rnp->cfg();
    if (!cli_rnp_set_generate_params(cfg)) {
        ERR_MSG("Key generation setup failed.");
        return false;
    }
    /* generate the primary key */
    rnp_op_generate_t genkey = NULL;
    rnp_key_handle_t  primary = NULL;
    rnp_key_handle_t  subkey = NULL;
    bool              res = false;

    if (rnp_op_generate_create(&genkey, rnp->ffi, cfg.get_cstr(CFG_KG_PRIMARY_ALG))) {
        ERR_MSG("Failed to initialize key generation.");
        return false;
    }
    if (username && rnp_op_generate_set_userid(genkey, username)) {
        ERR_MSG("Failed to set userid.");
        goto done;
    }
    if (cfg.has(CFG_KG_PRIMARY_BITS) &&
        rnp_op_generate_set_bits(genkey, cfg.get_int(CFG_KG_PRIMARY_BITS))) {
        ERR_MSG("Failed to set key bits.");
        goto done;
    }
    if (cfg.has(CFG_KG_PRIMARY_CURVE) &&
        rnp_op_generate_set_curve(genkey, cfg.get_cstr(CFG_KG_PRIMARY_CURVE))) {
        ERR_MSG("Failed to set key curve.");
        goto done;
    }
    if (cfg.has(CFG_KG_PRIMARY_EXPIRATION)) {
        uint32_t expiration = 0;
        if (!cfg.get_expiration(CFG_KG_PRIMARY_EXPIRATION, expiration) ||
            rnp_op_generate_set_expiration(genkey, expiration)) {
            ERR_MSG("Failed to set primary key expiration.");
            goto done;
        }
    }
    // TODO : set DSA qbits
    if (rnp_op_generate_set_hash(genkey, cfg.get_cstr(CFG_KG_HASH))) {
        ERR_MSG("Failed to set hash algorithm.");
        goto done;
    }

    fprintf(rnp->userio_out, "Generating a new key...\n");
    if (rnp_op_generate_execute(genkey) || rnp_op_generate_get_key(genkey, &primary)) {
        ERR_MSG("Primary key generation failed.");
        goto done;
    }

    if (!cfg.has(CFG_KG_SUBKEY_ALG)) {
        res = true;
        goto done;
    }

    rnp_op_generate_destroy(genkey);
    genkey = NULL;
    if (rnp_op_generate_subkey_create(
          &genkey, rnp->ffi, primary, cfg.get_cstr(CFG_KG_SUBKEY_ALG))) {
        ERR_MSG("Failed to initialize subkey generation.");
        goto done;
    }
    if (cfg.has(CFG_KG_SUBKEY_BITS) &&
        rnp_op_generate_set_bits(genkey, cfg.get_int(CFG_KG_SUBKEY_BITS))) {
        ERR_MSG("Failed to set subkey bits.");
        goto done;
    }
    if (cfg.has(CFG_KG_SUBKEY_CURVE) &&
        rnp_op_generate_set_curve(genkey, cfg.get_cstr(CFG_KG_SUBKEY_CURVE))) {
        ERR_MSG("Failed to set subkey curve.");
        goto done;
    }
    if (cfg.has(CFG_KG_SUBKEY_EXPIRATION)) {
        uint32_t expiration = 0;
        if (!cfg.get_expiration(CFG_KG_SUBKEY_EXPIRATION, expiration) ||
            rnp_op_generate_set_expiration(genkey, expiration)) {
            ERR_MSG("Failed to set subkey expiration.");
            goto done;
        }
    }
    // TODO : set DSA qbits
    if (rnp_op_generate_set_hash(genkey, cfg.get_cstr(CFG_KG_HASH))) {
        ERR_MSG("Failed to set hash algorithm.");
        goto done;
    }
    if (rnp_op_generate_execute(genkey) || rnp_op_generate_get_key(genkey, &subkey)) {
        ERR_MSG("Subkey generation failed.");
        goto done;
    }

    // protect
    for (auto key : {primary, subkey}) {
        char *password = NULL;
        if (rnp_request_password(rnp->ffi, key, "protect", &password)) {
            ERR_MSG("Failed to obtain protection password.");
            goto done;
        }
        if (*password) {
            rnp_result_t ret = rnp_key_protect(key,
                                               password,
                                               cfg.get_cstr(CFG_KG_PROT_ALG),
                                               NULL,
                                               cfg.get_cstr(CFG_KG_PROT_HASH),
                                               cfg.get_int(CFG_KG_PROT_ITERATIONS));
            rnp_buffer_clear(password, strlen(password) + 1);
            rnp_buffer_destroy(password);
            if (ret) {
                ERR_MSG("Failed to protect key.");
                goto done;
            }
        } else {
            rnp_buffer_destroy(password);
        }
    }
    res = cli_rnp_save_keyrings(rnp);
done:
    if (res) {
        cli_rnp_print_key_info(stdout, rnp->ffi, primary, true, false);
        if (subkey) {
            cli_rnp_print_key_info(stdout, rnp->ffi, subkey, true, false);
        }
    }
    rnp_op_generate_destroy(genkey);
    rnp_key_handle_destroy(primary);
    rnp_key_handle_destroy(subkey);
    return res;
}

static bool
key_matches_string(rnp_key_handle_t handle, const std::string &str)
{
    bool   matches = false;
    char * id = NULL;
    size_t idlen = 0;
#ifndef RNP_USE_STD_REGEX
    regex_t r = {};
#else
    std::regex re;
#endif
    size_t uid_count = 0;
    bool   boolres = false;

    if (str.empty()) {
        matches = true;
        goto done;
    }
    if (rnp::is_hex(str) && (str.length() >= RNP_KEYID_SIZE)) {
        std::string hexstr = rnp::strip_hex(str);
        size_t      len = hexstr.length();

        /* check whether it's key id */
        if ((len == RNP_KEYID_SIZE * 2) || (len == RNP_KEYID_SIZE)) {
            if (rnp_key_get_keyid(handle, &id)) {
                goto done;
            }
            if ((idlen = strlen(id)) < len) {
                goto done;
            }
            if (strncasecmp(hexstr.c_str(), id + idlen - len, len) == 0) {
                matches = true;
                goto done;
            }
            rnp_buffer_destroy(id);
            id = NULL;
        }

        /* check fingerprint */
        if (len == RNP_FP_SIZE * 2) {
            if (rnp_key_get_fprint(handle, &id)) {
                goto done;
            }
            if (strlen(id) != len) {
                goto done;
            }
            if (strncasecmp(hexstr.c_str(), id, len) == 0) {
                matches = true;
                goto done;
            }
            rnp_buffer_destroy(id);
            id = NULL;
        }

        /* check grip */
        if (len == RNP_GRIP_SIZE * 2) {
            if (rnp_key_get_grip(handle, &id)) {
                goto done;
            }
            if (strlen(id) != len) {
                goto done;
            }
            if (strncasecmp(hexstr.c_str(), id, len) == 0) {
                matches = true;
                goto done;
            }
            rnp_buffer_destroy(id);
            id = NULL;
        }
        /* let then search for hex userid */
    }

    /* no need to check for userid over the subkey */
    if (rnp_key_is_sub(handle, &boolres) || boolres) {
        goto done;
    }
    if (rnp_key_get_uid_count(handle, &uid_count) || (uid_count == 0)) {
        goto done;
    }

#ifndef RNP_USE_STD_REGEX
    /* match on full name or email address as a NOSUB, ICASE regexp */
    if (regcomp(&r, cli_rnp_unescape_for_regcomp(str).c_str(), REG_EXTENDED | REG_ICASE) !=
        0) {
        goto done;
    }
#else
    try {
        re.assign(str, std::regex_constants::ECMAScript | std::regex_constants::icase);
    } catch (const std::exception &e) {
        ERR_MSG("Invalid regular expression : %s, error %s.", str.c_str(), e.what());
        goto done;
    }
#endif

    for (size_t idx = 0; idx < uid_count; idx++) {
        if (rnp_key_get_uid_at(handle, idx, &id)) {
            goto regdone;
        }
#ifndef RNP_USE_STD_REGEX
        if (regexec(&r, id, 0, NULL, 0) == 0) {
            matches = true;
            goto regdone;
        }
#else
        if (std::regex_search(id, re)) {
            matches = true;
            goto regdone;
        }
#endif
        rnp_buffer_destroy(id);
        id = NULL;
    }

regdone:
#ifndef RNP_USE_STD_REGEX
    regfree(&r);
#endif
done:
    rnp_buffer_destroy(id);
    return matches;
}

static bool
key_matches_flags(rnp_key_handle_t key, int flags)
{
    /* check whether secret key search is requested */
    bool secret = false;
    if (rnp_key_have_secret(key, &secret)) {
        return false;
    }
    if ((flags & CLI_SEARCH_SECRET) && !secret) {
        return false;
    }
    /* check whether no subkeys allowed */
    bool subkey = false;
    if (rnp_key_is_sub(key, &subkey)) {
        return false;
    }
    if (!subkey) {
        return true;
    }
    if (!(flags & CLI_SEARCH_SUBKEYS)) {
        return false;
    }
    /* check whether subkeys should be put after primary (if it is available) */
    if ((flags & CLI_SEARCH_SUBKEYS_AFTER) != CLI_SEARCH_SUBKEYS_AFTER) {
        return true;
    }

    char *grip = NULL;
    if (rnp_key_get_primary_grip(key, &grip)) {
        return false;
    }
    if (!grip) {
        return true;
    }
    rnp_buffer_destroy(grip);
    return false;
}

void
clear_key_handles(std::vector<rnp_key_handle_t> &keys)
{
    for (auto handle : keys) {
        rnp_key_handle_destroy(handle);
    }
    keys.clear();
}

static bool
add_key_to_array(rnp_ffi_t                      ffi,
                 std::vector<rnp_key_handle_t> &keys,
                 rnp_key_handle_t               key,
                 int                            flags)
{
    bool subkey = false;
    bool subkeys = (flags & CLI_SEARCH_SUBKEYS_AFTER) == CLI_SEARCH_SUBKEYS_AFTER;
    if (rnp_key_is_sub(key, &subkey)) {
        return false;
    }

    try {
        keys.push_back(key);
    } catch (const std::exception &e) {
        ERR_MSG("%s", e.what());
        return false;
    }
    if (!subkeys || subkey) {
        return true;
    }

    std::vector<rnp_key_handle_t> subs;
    size_t                        sub_count = 0;
    if (rnp_key_get_subkey_count(key, &sub_count)) {
        goto error;
    }

    try {
        for (size_t i = 0; i < sub_count; i++) {
            rnp_key_handle_t sub_handle = NULL;
            if (rnp_key_get_subkey_at(key, i, &sub_handle)) {
                goto error;
            }
            subs.push_back(sub_handle);
        }
        std::move(subs.begin(), subs.end(), std::back_inserter(keys));
    } catch (const std::exception &e) {
        ERR_MSG("%s", e.what());
        goto error;
    }
    return true;
error:
    keys.pop_back();
    clear_key_handles(subs);
    return false;
}

bool
cli_rnp_keys_matching_string(cli_rnp_t *                    rnp,
                             std::vector<rnp_key_handle_t> &keys,
                             const std::string &            str,
                             int                            flags)
{
    bool                      res = false;
    rnp_identifier_iterator_t it = NULL;
    rnp_key_handle_t          handle = NULL;
    const char *              fp = NULL;

    /* iterate through the keys */
    if (rnp_identifier_iterator_create(rnp->ffi, &it, "fingerprint")) {
        return false;
    }

    while (!rnp_identifier_iterator_next(it, &fp)) {
        if (!fp) {
            break;
        }
        if (rnp_locate_key(rnp->ffi, "fingerprint", fp, &handle) || !handle) {
            goto done;
        }
        if (!key_matches_flags(handle, flags) || !key_matches_string(handle, str.c_str())) {
            rnp_key_handle_destroy(handle);
            continue;
        }
        if (!add_key_to_array(rnp->ffi, keys, handle, flags)) {
            rnp_key_handle_destroy(handle);
            goto done;
        }
        if (flags & CLI_SEARCH_FIRST_ONLY) {
            res = true;
            goto done;
        }
    }
    res = !keys.empty();
done:
    rnp_identifier_iterator_destroy(it);
    return res;
}

bool
cli_rnp_keys_matching_strings(cli_rnp_t *                     rnp,
                              std::vector<rnp_key_handle_t> & keys,
                              const std::vector<std::string> &strs,
                              int                             flags)
{
    bool res = false;
    clear_key_handles(keys);

    for (const std::string &str : strs) {
        if (!cli_rnp_keys_matching_string(rnp, keys, str, flags & ~CLI_SEARCH_DEFAULT)) {
            ERR_MSG("Cannot find key matching \"%s\"", str.c_str());
            goto done;
        }
    }

    /* search for default key */
    if (keys.empty() && (flags & CLI_SEARCH_DEFAULT)) {
        if (rnp->defkey().empty()) {
            ERR_MSG("No userid or default key for operation");
            goto done;
        }
        cli_rnp_keys_matching_string(rnp, keys, rnp->defkey(), flags & ~CLI_SEARCH_DEFAULT);
        if (keys.empty()) {
            ERR_MSG("Default key not found");
        }
    }
    res = !keys.empty();
done:
    if (!res) {
        clear_key_handles(keys);
    }
    return res;
}

static bool
rnp_cfg_set_ks_info(rnp_cfg &cfg)
{
    if (cfg.get_bool(CFG_KEYSTORE_DISABLED)) {
        cfg.set_str(CFG_KR_PUB_PATH, "");
        cfg.set_str(CFG_KR_SEC_PATH, "");
        cfg.set_str(CFG_KR_PUB_FORMAT, RNP_KEYSTORE_GPG);
        cfg.set_str(CFG_KR_SEC_FORMAT, RNP_KEYSTORE_GPG);
        return true;
    }

    /* getting path to keyrings. If it is specified by user in 'homedir' param then it is
     * considered as the final path */
    bool        defhomedir = false;
    std::string homedir = cfg.get_str(CFG_HOMEDIR);
    if (homedir.empty()) {
        homedir = rnp::path::HOME();
        defhomedir = true;
    }

    /* Check whether $HOME or homedir exists */
    struct stat st;
    if (rnp_stat(homedir.c_str(), &st) || rnp_access(homedir.c_str(), R_OK | W_OK)) {
        ERR_MSG("Home directory '%s' does not exist or is not writable!", homedir.c_str());
        return false;
    }

    /* creating home dir if needed */
    if (defhomedir) {
        char *rnphome = NULL;
        if (rnp_get_default_homedir(&rnphome)) {
            ERR_MSG("Failed to obtain default home directory.");
            return false;
        }
        homedir = rnphome;
        rnp_buffer_destroy(rnphome);
        if (!rnp::path::exists(homedir, true) && RNP_MKDIR(homedir.c_str(), 0700) == -1 &&
            errno != EEXIST) {
            ERR_MSG("Cannot mkdir '%s' errno = %d", homedir.c_str(), errno);
            return false;
        }
    }

    /* detecting key storage format */
    std::string ks_format = cfg.get_str(CFG_KEYSTOREFMT);
    if (ks_format.empty()) {
        char *pub_format = NULL;
        char *sec_format = NULL;
        char *pubpath = NULL;
        char *secpath = NULL;
        rnp_detect_homedir_info(homedir.c_str(), &pub_format, &pubpath, &sec_format, &secpath);
        bool detected = pub_format && sec_format && pubpath && secpath;
        if (detected) {
            cfg.set_str(CFG_KR_PUB_FORMAT, pub_format);
            cfg.set_str(CFG_KR_SEC_FORMAT, sec_format);
            cfg.set_str(CFG_KR_PUB_PATH, pubpath);
            cfg.set_str(CFG_KR_SEC_PATH, secpath);
        } else {
            /* default to GPG */
            ks_format = RNP_KEYSTORE_GPG;
        }
        rnp_buffer_destroy(pub_format);
        rnp_buffer_destroy(sec_format);
        rnp_buffer_destroy(pubpath);
        rnp_buffer_destroy(secpath);
        if (detected) {
            return true;
        }
    }

    std::string pub_format = RNP_KEYSTORE_GPG;
    std::string sec_format = RNP_KEYSTORE_GPG;
    std::string pubpath;
    std::string secpath;

    if (ks_format == RNP_KEYSTORE_GPG) {
        pubpath = rnp::path::append(homedir, PUBRING_GPG);
        secpath = rnp::path::append(homedir, SECRING_GPG);
    } else if (ks_format == RNP_KEYSTORE_GPG21) {
        pubpath = rnp::path::append(homedir, PUBRING_KBX);
        secpath = rnp::path::append(homedir, SECRING_G10);
        pub_format = RNP_KEYSTORE_KBX;
        sec_format = RNP_KEYSTORE_G10;
    } else if (ks_format == RNP_KEYSTORE_KBX) {
        pubpath = rnp::path::append(homedir, PUBRING_KBX);
        secpath = rnp::path::append(homedir, SECRING_KBX);
        pub_format = RNP_KEYSTORE_KBX;
        sec_format = RNP_KEYSTORE_KBX;
    } else if (ks_format == RNP_KEYSTORE_G10) {
        pubpath = rnp::path::append(homedir, PUBRING_G10);
        secpath = rnp::path::append(homedir, SECRING_G10);
        pub_format = RNP_KEYSTORE_G10;
        sec_format = RNP_KEYSTORE_G10;
    } else {
        ERR_MSG("Unsupported keystore format: \"%s\"", ks_format.c_str());
        return false;
    }

    /* Check whether homedir is empty */
    if (rnp::path::empty(homedir)) {
        ERR_MSG("Keyring directory '%s' is empty.\nUse \"rnpkeys\" command to generate a new "
                "key or import existing keys from the file or GnuPG keyrings.",
                homedir.c_str());
    }

    cfg.set_str(CFG_KR_PUB_PATH, pubpath);
    cfg.set_str(CFG_KR_SEC_PATH, secpath);
    cfg.set_str(CFG_KR_PUB_FORMAT, pub_format);
    cfg.set_str(CFG_KR_SEC_FORMAT, sec_format);
    return true;
}

static void
rnp_cfg_set_defkey(rnp_cfg &cfg)
{
    /* If a userid has been given, we'll use it. */
    std::string userid = cfg.get_count(CFG_USERID) ? cfg.get_str(CFG_USERID, 0) : "";
    if (!userid.empty()) {
        cfg.set_str(CFG_KR_DEF_KEY, userid);
    }
}

bool
cli_cfg_set_keystore_info(rnp_cfg &cfg)
{
    /* detecting keystore paths and format */
    if (!rnp_cfg_set_ks_info(cfg)) {
        return false;
    }

    /* default key/userid */
    rnp_cfg_set_defkey(cfg);
    return true;
}

static bool
is_stdinout_spec(const std::string &spec)
{
    return spec.empty() || (spec == "-");
}

rnp_input_t
cli_rnp_input_from_specifier(cli_rnp_t &rnp, const std::string &spec, bool *is_path)
{
    rnp_input_t  input = NULL;
    rnp_result_t res = RNP_ERROR_GENERIC;
    bool         path = false;
    if (is_stdinout_spec(spec)) {
        /* input from stdin */
        res = rnp_input_from_stdin(&input);
    } else if ((spec.size() > 4) && (spec.compare(0, 4, "env:") == 0)) {
        /* input from an environment variable */
        const char *envval = getenv(spec.c_str() + 4);
        if (!envval) {
            ERR_MSG("Failed to get value of the environment variable '%s'.", spec.c_str() + 4);
            return NULL;
        }
        res = rnp_input_from_memory(&input, (const uint8_t *) envval, strlen(envval), true);
    } else {
        /* input from path */
        res = rnp_input_from_path(&input, spec.c_str());
        path = true;
    }

    if (res) {
        return NULL;
    }
    if (is_path) {
        *is_path = path;
    }
    return input;
}

rnp_output_t
cli_rnp_output_to_specifier(cli_rnp_t &rnp, const std::string &spec, bool discard)
{
    rnp_output_t output = NULL;
    rnp_result_t res = RNP_ERROR_GENERIC;
    std::string  path = spec;
    if (discard) {
        res = rnp_output_to_null(&output);
    } else if (is_stdinout_spec(spec)) {
        res = rnp_output_to_stdout(&output);
    } else if (!rnp_get_output_filename(spec, path, rnp)) {
        if (spec.empty()) {
            ERR_MSG("Operation failed: no output filename specified");
        } else {
            ERR_MSG("Operation failed: file '%s' already exists.", spec.c_str());
        }
        res = RNP_ERROR_BAD_PARAMETERS;
    } else {
        res = rnp_output_to_file(&output, path.c_str(), RNP_OUTPUT_FILE_OVERWRITE);
    }
    return res ? NULL : output;
}

bool
cli_rnp_export_keys(cli_rnp_t *rnp, const char *filter)
{
    bool                          secret = rnp->cfg().get_bool(CFG_SECRET);
    int                           flags = secret ? CLI_SEARCH_SECRET : 0;
    std::vector<rnp_key_handle_t> keys;

    if (!cli_rnp_keys_matching_string(rnp, keys, filter, flags)) {
        ERR_MSG("Key(s) matching '%s' not found.", filter);
        return false;
    }

    rnp_output_t output = NULL;
    rnp_output_t armor = NULL;
    uint32_t     base_flags = secret ? RNP_KEY_EXPORT_SECRET : RNP_KEY_EXPORT_PUBLIC;
    bool         result = false;

    output = cli_rnp_output_to_specifier(*rnp, rnp->cfg().get_str(CFG_OUTFILE));
    if (!output) {
        goto done;
    }

    /* We need single armored stream for all of the keys */
    if (rnp_output_to_armor(output, &armor, secret ? "secret key" : "public key")) {
        goto done;
    }

    for (auto key : keys) {
        uint32_t flags = base_flags;
        bool     primary = false;

        if (rnp_key_is_primary(key, &primary)) {
            goto done;
        }
        if (primary) {
            flags = flags | RNP_KEY_EXPORT_SUBKEYS;
        }

        if (rnp_key_export(key, armor, flags)) {
            goto done;
        }
    }
    result = !rnp_output_finish(armor);
done:
    rnp_output_destroy(armor);
    rnp_output_destroy(output);
    clear_key_handles(keys);
    return result;
}

bool
cli_rnp_export_revocation(cli_rnp_t *rnp, const char *key)
{
    std::vector<rnp_key_handle_t> keys;
    if (!cli_rnp_keys_matching_string(rnp, keys, key, 0)) {
        ERR_MSG("Key matching '%s' not found.", key);
        return false;
    }
    if (keys.size() > 1) {
        ERR_MSG("Ambiguous input: too many keys found for '%s'.", key);
        clear_key_handles(keys);
        return false;
    }
    rnp_output_t output = NULL;
    bool         result = false;

    output = cli_rnp_output_to_specifier(*rnp, rnp->cfg().get_str(CFG_OUTFILE));
    if (!output) {
        goto done;
    }

    result = !rnp_key_export_revocation(keys[0],
                                        output,
                                        RNP_KEY_EXPORT_ARMORED,
                                        rnp->cfg().get_cstr(CFG_HASH),
                                        rnp->cfg().get_cstr(CFG_REV_TYPE),
                                        rnp->cfg().get_cstr(CFG_REV_REASON));
done:
    rnp_output_destroy(output);
    clear_key_handles(keys);
    return result;
}

bool
cli_rnp_revoke_key(cli_rnp_t *rnp, const char *key)
{
    std::vector<rnp_key_handle_t> keys;
    if (!cli_rnp_keys_matching_string(rnp, keys, key, CLI_SEARCH_SUBKEYS)) {
        ERR_MSG("Key matching '%s' not found.", key);
        return false;
    }
    bool         res = false;
    bool         revoked = false;
    rnp_result_t ret = 0;

    if (keys.size() > 1) {
        ERR_MSG("Ambiguous input: too many keys found for '%s'.", key);
        goto done;
    }
    if (rnp_key_is_revoked(keys[0], &revoked)) {
        ERR_MSG("Error getting key revocation status.");
        goto done;
    }
    if (revoked && !rnp->cfg().get_bool(CFG_FORCE)) {
        ERR_MSG("Error: key '%s' is revoked already. Use --force to generate another "
                "revocation signature.",
                key);
        goto done;
    }

    ret = rnp_key_revoke(keys[0],
                         0,
                         rnp->cfg().get_cstr(CFG_HASH),
                         rnp->cfg().get_cstr(CFG_REV_TYPE),
                         rnp->cfg().get_cstr(CFG_REV_REASON));
    if (ret) {
        ERR_MSG("Failed to revoke a key: error %d", (int) ret);
        goto done;
    }
    res = cli_rnp_save_keyrings(rnp);
    /* print info about the revoked key */
    if (res) {
        bool  subkey = false;
        char *grip = NULL;
        if (rnp_key_is_sub(keys[0], &subkey)) {
            ERR_MSG("Failed to get key info");
            goto done;
        }
        ret =
          subkey ? rnp_key_get_primary_grip(keys[0], &grip) : rnp_key_get_grip(keys[0], &grip);
        if (ret || !grip) {
            ERR_MSG("Failed to get primary key grip.");
            goto done;
        }
        clear_key_handles(keys);
        if (!cli_rnp_keys_matching_string(rnp, keys, grip, CLI_SEARCH_SUBKEYS_AFTER)) {
            ERR_MSG("Failed to search for revoked key.");
            rnp_buffer_destroy(grip);
            goto done;
        }
        rnp_buffer_destroy(grip);
        for (auto handle : keys) {
            cli_rnp_print_key_info(rnp->userio_out, rnp->ffi, handle, false, false);
        }
    }
done:
    clear_key_handles(keys);
    return res;
}

bool
cli_rnp_remove_key(cli_rnp_t *rnp, const char *key)
{
    std::vector<rnp_key_handle_t> keys;
    if (!cli_rnp_keys_matching_string(rnp, keys, key, CLI_SEARCH_SUBKEYS)) {
        ERR_MSG("Key matching '%s' not found.", key);
        return false;
    }
    bool         res = false;
    bool         secret = false;
    bool         primary = false;
    uint32_t     flags = RNP_KEY_REMOVE_PUBLIC;
    rnp_result_t ret = 0;

    if (keys.size() > 1) {
        ERR_MSG("Ambiguous input: too many keys found for '%s'.", key);
        goto done;
    }
    if (rnp_key_have_secret(keys[0], &secret)) {
        ERR_MSG("Error getting secret key presence.");
        goto done;
    }
    if (rnp_key_is_primary(keys[0], &primary)) {
        ERR_MSG("Key error.");
        goto done;
    }

    if (secret) {
        flags |= RNP_KEY_REMOVE_SECRET;
    }
    if (primary) {
        flags |= RNP_KEY_REMOVE_SUBKEYS;
    }

    if (secret && !rnp->cfg().get_bool(CFG_FORCE)) {
        if (!cli_rnp_get_confirmation(
              rnp,
              "Key '%s' has corresponding secret key. Do you really want to delete it?",
              key)) {
            goto done;
        }
    }

    ret = rnp_key_remove(keys[0], flags);

    if (ret) {
        ERR_MSG("Failed to remove the key: error %d", (int) ret);
        goto done;
    }
    res = cli_rnp_save_keyrings(rnp);
done:
    clear_key_handles(keys);
    return res;
}

bool
cli_rnp_add_key(cli_rnp_t *rnp)
{
    const std::string &path = rnp->cfg().get_str(CFG_KEYFILE);
    if (path.empty()) {
        return false;
    }

    rnp_input_t input = cli_rnp_input_from_specifier(*rnp, path, NULL);
    if (!input) {
        ERR_MSG("failed to open key from %s", path.c_str());
        return false;
    }

    bool res = !rnp_import_keys(
      rnp->ffi, input, RNP_LOAD_SAVE_PUBLIC_KEYS | RNP_LOAD_SAVE_SECRET_KEYS, NULL);
    rnp_input_destroy(input);

    // set default key if we didn't have one
    if (res && rnp->defkey().empty()) {
        rnp->set_defkey();
    }
    return res;
}

static bool
strip_extension(std::string &src)
{
    size_t dpos = src.find_last_of('.');
    if (dpos == std::string::npos) {
        return false;
    }
    src.resize(dpos);
    return true;
}

static bool
has_extension(const std::string &path, const std::string &ext)
{
    if (path.length() < ext.length()) {
        return false;
    }
    return path.compare(path.length() - ext.length(), ext.length(), ext) == 0;
}

static std::string
output_extension(const rnp_cfg &cfg, Operation op)
{
    switch (op) {
    case Operation::EncryptOrSign: {
        bool armor = cfg.get_bool(CFG_ARMOR);
        if (cfg.get_bool(CFG_DETACHED)) {
            return armor ? EXT_ASC : EXT_SIG;
        }
        if (cfg.get_bool(CFG_CLEARTEXT)) {
            return EXT_ASC;
        }
        return armor ? EXT_ASC : EXT_PGP;
    }
    case Operation::Enarmor:
        return EXT_ASC;
    default:
        return "";
    }
}

static bool
has_pgp_extension(const std::string &path)
{
    return has_extension(path, EXT_PGP) || has_extension(path, EXT_ASC) ||
           has_extension(path, EXT_GPG);
}

static std::string
output_strip_extension(Operation op, const std::string &in)
{
    std::string out = in;
    if ((op == Operation::Verify) && (has_pgp_extension(out))) {
        strip_extension(out);
        return out;
    }
    if ((op == Operation::Dearmor) && (has_extension(out, EXT_ASC))) {
        strip_extension(out);
        return out;
    }
    return "";
}

static std::string
extract_filename(const std::string path)
{
    size_t lpos = path.find_last_of("/\\");
    if (lpos == std::string::npos) {
        return path;
    }
    return path.substr(lpos + 1);
}

bool
cli_rnp_t::init_io(Operation op, rnp_input_t *input, rnp_output_t *output)
{
    const std::string &in = cfg().get_str(CFG_INFILE);
    bool               is_pathin = true;
    if (input) {
        *input = cli_rnp_input_from_specifier(*this, in, &is_pathin);
        if (!*input) {
            return false;
        }
    }
    /* Update CFG_SETFNAME to insert into literal packet */
    if (!cfg().has(CFG_SETFNAME) && !is_pathin) {
        cfg().set_str(CFG_SETFNAME, "");
    }

    if (!output) {
        return true;
    }
    std::string out = cfg().get_str(CFG_OUTFILE);
    bool discard = (op == Operation::Verify) && out.empty() && cfg().get_bool(CFG_NO_OUTPUT);

    if (out.empty() && is_pathin && !discard) {
        /* Attempt to guess whether to add or strip extension for known cases */
        std::string ext = output_extension(cfg(), op);
        if (!ext.empty()) {
            out = in + ext;
        } else {
            out = output_strip_extension(op, in);
        }
    }

    *output = cli_rnp_output_to_specifier(*this, out, discard);
    if (!*output && input) {
        rnp_input_destroy(*input);
        *input = NULL;
    }
    return *output;
}

bool
cli_rnp_dump_file(cli_rnp_t *rnp)
{
    rnp_input_t  input = NULL;
    rnp_output_t output = NULL;
    uint32_t     flags = 0;
    uint32_t     jflags = 0;

    if (rnp->cfg().get_bool(CFG_GRIPS)) {
        flags |= RNP_DUMP_GRIP;
        jflags |= RNP_JSON_DUMP_GRIP;
    }
    if (rnp->cfg().get_bool(CFG_MPIS)) {
        flags |= RNP_DUMP_MPI;
        jflags |= RNP_JSON_DUMP_MPI;
    }
    if (rnp->cfg().get_bool(CFG_RAW)) {
        flags |= RNP_DUMP_RAW;
        jflags |= RNP_JSON_DUMP_RAW;
    }

    rnp_result_t ret = 0;
    if (!rnp->init_io(Operation::Dump, &input, &output)) {
        ERR_MSG("failed to open source or create output");
        ret = 1;
        goto done;
    }

    if (rnp->cfg().get_bool(CFG_JSON)) {
        char *json = NULL;
        ret = rnp_dump_packets_to_json(input, jflags, &json);
        if (!ret) {
            size_t len = strlen(json);
            size_t written = 0;
            ret = rnp_output_write(output, json, len, &written);
            if (written < len) {
                ret = RNP_ERROR_WRITE;
            }
            // add trailing empty line
            if (!ret) {
                ret = rnp_output_write(output, "\n", 1, &written);
            }
            if (written < 1) {
                ret = RNP_ERROR_WRITE;
            }
            rnp_buffer_destroy(json);
        }
    } else {
        ret = rnp_dump_packets_to_output(input, output, flags);
    }
    rnp_input_destroy(input);
    rnp_output_destroy(output);
done:
    return !ret;
}

bool
cli_rnp_armor_file(cli_rnp_t *rnp)
{
    rnp_input_t  input = NULL;
    rnp_output_t output = NULL;

    if (!rnp->init_io(Operation::Enarmor, &input, &output)) {
        ERR_MSG("failed to open source or create output");
        return false;
    }
    rnp_result_t ret = rnp_enarmor(input, output, rnp->cfg().get_cstr(CFG_ARMOR_DATA_TYPE));
    rnp_input_destroy(input);
    rnp_output_destroy(output);
    return !ret;
}

bool
cli_rnp_dearmor_file(cli_rnp_t *rnp)
{
    rnp_input_t  input = NULL;
    rnp_output_t output = NULL;

    if (!rnp->init_io(Operation::Dearmor, &input, &output)) {
        ERR_MSG("failed to open source or create output");
        return false;
    }

    rnp_result_t ret = rnp_dearmor(input, output);
    rnp_input_destroy(input);
    rnp_output_destroy(output);
    return !ret;
}

static bool
cli_rnp_sign(const rnp_cfg &cfg, cli_rnp_t *rnp, rnp_input_t input, rnp_output_t output)
{
    rnp_op_sign_t op = NULL;
    rnp_result_t  ret = RNP_ERROR_GENERIC;
    bool          cleartext = cfg.get_bool(CFG_CLEARTEXT);
    bool          detached = cfg.get_bool(CFG_DETACHED);

    if (cleartext) {
        ret = rnp_op_sign_cleartext_create(&op, rnp->ffi, input, output);
    } else if (detached) {
        ret = rnp_op_sign_detached_create(&op, rnp->ffi, input, output);
    } else {
        ret = rnp_op_sign_create(&op, rnp->ffi, input, output);
    }

    if (ret) {
        ERR_MSG("failed to initialize signing");
        return false;
    }

    /* setup sign operation via cfg */
    bool                          res = false;
    std::vector<std::string>      signers;
    std::vector<rnp_key_handle_t> signkeys;

    if (!cleartext) {
        rnp_op_sign_set_armor(op, cfg.get_bool(CFG_ARMOR));
    }

    if (!cleartext && !detached) {
        if (cfg.has(CFG_SETFNAME)) {
            if (rnp_op_sign_set_file_name(op, cfg.get_str(CFG_SETFNAME).c_str())) {
                goto done;
            }
        } else if (cfg.has(CFG_INFILE)) {
            const std::string &fname = cfg.get_str(CFG_INFILE);
            if (rnp_op_sign_set_file_name(op, extract_filename(fname).c_str())) {
                goto done;
            }
            rnp_op_sign_set_file_mtime(op, rnp_filemtime(fname.c_str()));
        }
        if (rnp_op_sign_set_compression(op, cfg.get_cstr(CFG_ZALG), cfg.get_int(CFG_ZLEVEL))) {
            goto done;
        }
    }

    if (rnp_op_sign_set_hash(op, cfg.get_hashalg().c_str())) {
        goto done;
    }
    rnp_op_sign_set_creation_time(op, cfg.get_sig_creation());
    {
        uint32_t expiration = 0;
        if (cfg.get_expiration(CFG_EXPIRATION, expiration)) {
            rnp_op_sign_set_expiration_time(op, expiration);
        }
    }

    /* signing keys */
    signers = cfg.get_list(CFG_SIGNERS);
    if (!cli_rnp_keys_matching_strings(rnp,
                                       signkeys,
                                       signers,
                                       CLI_SEARCH_SECRET | CLI_SEARCH_DEFAULT |
                                         CLI_SEARCH_SUBKEYS | CLI_SEARCH_FIRST_ONLY)) {
        ERR_MSG("Failed to build signing keys list");
        goto done;
    }
    for (rnp_key_handle_t key : signkeys) {
        if (rnp_op_sign_add_signature(op, key, NULL)) {
            ERR_MSG("Failed to add signature");
            goto done;
        }
    }

    /* execute sign operation */
    res = !rnp_op_sign_execute(op);
done:
    clear_key_handles(signkeys);
    rnp_op_sign_destroy(op);
    return res;
}

static bool
cli_rnp_encrypt_and_sign(const rnp_cfg &cfg,
                         cli_rnp_t *    rnp,
                         rnp_input_t    input,
                         rnp_output_t   output)
{
    rnp_op_encrypt_t op = NULL;

    if (rnp_op_encrypt_create(&op, rnp->ffi, input, output)) {
        ERR_MSG("failed to initialize encryption");
        return false;
    }

    std::string                   fname;
    std::string                   aalg;
    std::vector<rnp_key_handle_t> enckeys;
    std::vector<rnp_key_handle_t> signkeys;
    bool                          res = false;
    rnp_result_t                  ret;

    rnp_op_encrypt_set_armor(op, cfg.get_bool(CFG_ARMOR));

    if (cfg.has(CFG_SETFNAME)) {
        if (rnp_op_encrypt_set_file_name(op, cfg.get_str(CFG_SETFNAME).c_str())) {
            goto done;
        }
    } else if (cfg.has(CFG_INFILE)) {
        const std::string &fname = cfg.get_str(CFG_INFILE);
        if (rnp_op_encrypt_set_file_name(op, extract_filename(fname).c_str())) {
            goto done;
        }
        rnp_op_encrypt_set_file_mtime(op, rnp_filemtime(fname.c_str()));
    }

    if (rnp_op_encrypt_set_compression(op, cfg.get_cstr(CFG_ZALG), cfg.get_int(CFG_ZLEVEL))) {
        goto done;
    }
    if (rnp_op_encrypt_set_cipher(op, cfg.get_cstr(CFG_CIPHER))) {
        goto done;
    }
    if (rnp_op_encrypt_set_hash(op, cfg.get_hashalg().c_str())) {
        goto done;
    }
    aalg = cfg.has(CFG_AEAD) ? cfg.get_str(CFG_AEAD) : "None";
    if (rnp_op_encrypt_set_aead(op, aalg.c_str())) {
        goto done;
    }
    if (cfg.has(CFG_AEAD_CHUNK) &&
        rnp_op_encrypt_set_aead_bits(op, cfg.get_int(CFG_AEAD_CHUNK))) {
        goto done;
    }
    if (cfg.has(CFG_NOWRAP) && rnp_op_encrypt_set_flags(op, RNP_ENCRYPT_NOWRAP)) {
        goto done;
    }

    /* adding passwords if password-based encryption is used */
    if (cfg.get_bool(CFG_ENCRYPT_SK)) {
        std::string halg = cfg.get_hashalg();
        std::string ealg = cfg.get_str(CFG_CIPHER);
        size_t      iterations = cfg.get_int(CFG_S2K_ITER);
        size_t      msec = cfg.get_int(CFG_S2K_MSEC);

        if (msec != DEFAULT_S2K_MSEC) {
            if (rnp_calculate_iterations(halg.c_str(), msec, &iterations)) {
                ERR_MSG("Failed to calculate S2K iterations");
                goto done;
            }
        }

        for (int i = 0; i < cfg.get_int(CFG_PASSWORDC, 1); i++) {
            if (rnp_op_encrypt_add_password(
                  op, NULL, halg.c_str(), iterations, ealg.c_str())) {
                ERR_MSG("Failed to add encrypting password");
                goto done;
            }
        }
    }

    /* adding encrypting keys if pk-encryption is used */
    if (cfg.get_bool(CFG_ENCRYPT_PK)) {
        std::vector<std::string> keynames = cfg.get_list(CFG_RECIPIENTS);
        if (!cli_rnp_keys_matching_strings(rnp,
                                           enckeys,
                                           keynames,
                                           CLI_SEARCH_DEFAULT | CLI_SEARCH_SUBKEYS |
                                             CLI_SEARCH_FIRST_ONLY)) {
            ERR_MSG("Failed to build recipients key list");
            goto done;
        }
        for (rnp_key_handle_t key : enckeys) {
            if (rnp_op_encrypt_add_recipient(op, key)) {
                ERR_MSG("Failed to add recipient");
                goto done;
            }
        }
    }

    /* adding signatures if encrypt-and-sign is used */
    if (cfg.get_bool(CFG_SIGN_NEEDED)) {
        rnp_op_encrypt_set_creation_time(op, cfg.get_sig_creation());
        uint32_t expiration;
        if (cfg.get_expiration(CFG_EXPIRATION, expiration)) {
            rnp_op_encrypt_set_expiration_time(op, expiration);
        }

        /* signing keys */
        std::vector<std::string> keynames = cfg.get_list(CFG_SIGNERS);
        if (!cli_rnp_keys_matching_strings(rnp,
                                           signkeys,
                                           keynames,
                                           CLI_SEARCH_SECRET | CLI_SEARCH_DEFAULT |
                                             CLI_SEARCH_SUBKEYS | CLI_SEARCH_FIRST_ONLY)) {
            ERR_MSG("Failed to build signing keys list");
            goto done;
        }
        for (rnp_key_handle_t key : signkeys) {
            if (rnp_op_encrypt_add_signature(op, key, NULL)) {
                ERR_MSG("Failed to add signature");
                goto done;
            }
        }
    }

    /* execute encrypt or encrypt-and-sign operation */
    ret = rnp_op_encrypt_execute(op);
    res = (ret == RNP_SUCCESS);
    if (ret != RNP_SUCCESS) {
        ERR_MSG("Operation failed: %s", rnp_result_to_string(ret));
    }
done:
    clear_key_handles(signkeys);
    clear_key_handles(enckeys);
    rnp_op_encrypt_destroy(op);
    return res;
}

bool
cli_rnp_setup(cli_rnp_t *rnp)
{
    /* unset CFG_PASSWD and empty CFG_PASSWD are different cases */
    if (rnp->cfg().has(CFG_PASSWD)) {
        const std::string &passwd = rnp->cfg().get_str(CFG_PASSWD);
        if (rnp_ffi_set_pass_provider(
              rnp->ffi, ffi_pass_callback_string, (void *) passwd.c_str())) {
            return false;
        }
    }
    rnp->pswdtries = rnp->cfg().get_pswdtries();
    return true;
}

bool
cli_rnp_check_weak_hash(cli_rnp_t *rnp)
{
    if (rnp->cfg().has(CFG_WEAK_HASH)) {
        return true;
    }

    uint32_t security_level = 0;

    if (rnp_get_security_rule(rnp->ffi,
                              RNP_FEATURE_HASH_ALG,
                              rnp->cfg().get_hashalg().c_str(),
                              rnp->cfg().time(),
                              NULL,
                              NULL,
                              &security_level)) {
        ERR_MSG("Failed to get security rules for hash algorithm \'%s\'!",
                rnp->cfg().get_hashalg().c_str());
        return false;
    }

    if (security_level < RNP_SECURITY_DEFAULT) {
        ERR_MSG("Hash algorithm \'%s\' is cryptographically weak!",
                rnp->cfg().get_hashalg().c_str());
        return false;
    }
    /* TODO: check other weak algorithms and key sizes */
    return true;
}

bool
cli_rnp_protect_file(cli_rnp_t *rnp)
{
    rnp_input_t  input = NULL;
    rnp_output_t output = NULL;

    if (!rnp->init_io(Operation::EncryptOrSign, &input, &output)) {
        ERR_MSG("failed to open source or create output");
        return false;
    }

    bool res = false;
    bool sign = rnp->cfg().get_bool(CFG_SIGN_NEEDED);
    bool encrypt = rnp->cfg().get_bool(CFG_ENCRYPT_PK) || rnp->cfg().get_bool(CFG_ENCRYPT_SK);
    if (sign && !encrypt) {
        res = cli_rnp_sign(rnp->cfg(), rnp, input, output);
    } else if (encrypt) {
        res = cli_rnp_encrypt_and_sign(rnp->cfg(), rnp, input, output);
    } else {
        ERR_MSG("No operation specified");
    }

    rnp_input_destroy(input);
    rnp_output_destroy(output);
    return res;
}

/* helper function which prints something like 'using RSA (Sign-Only) key 0x0102030405060708 */
static void
cli_rnp_print_sig_key_info(FILE *resfp, rnp_signature_handle_t sig)
{
    char *keyid = NULL;
    char *alg = NULL;

    (void) rnp_signature_get_keyid(sig, &keyid);
    rnp::lowercase(keyid);
    (void) rnp_signature_get_alg(sig, &alg);

    fprintf(resfp,
            "using %s key %s\n",
            cli_rnp_normalize_key_alg(alg),
            keyid ? keyid : "0000000000000000");
    rnp_buffer_destroy(keyid);
    rnp_buffer_destroy(alg);
}

static void
cli_rnp_print_signatures(cli_rnp_t *rnp, const std::vector<rnp_op_verify_signature_t> &sigs)
{
    unsigned    invalidc = 0;
    unsigned    unknownc = 0;
    std::string title = "UNKNOWN signature";
    FILE *      resfp = rnp->resfp;

    for (auto sig : sigs) {
        rnp_result_t status = rnp_op_verify_signature_get_status(sig);
        switch (status) {
        case RNP_SUCCESS:
            title = "Good signature";
            break;
        case RNP_ERROR_SIGNATURE_EXPIRED:
            title = "EXPIRED signature";
            invalidc++;
            break;
        case RNP_ERROR_SIGNATURE_INVALID:
            title = "BAD signature";
            invalidc++;
            break;
        case RNP_ERROR_KEY_NOT_FOUND:
            title = "NO PUBLIC KEY for signature";
            unknownc++;
            break;
        case RNP_ERROR_SIGNATURE_UNKNOWN:
            title = "UNKNOWN signature";
            unknownc++;
            break;
        default:
            title = "UNKNOWN signature status";
            break;
        }

        if (status == RNP_ERROR_SIGNATURE_UNKNOWN) {
            fprintf(resfp, "%s\n", title.c_str());
            continue;
        }

        uint32_t create = 0;
        uint32_t expiry = 0;
        rnp_op_verify_signature_get_times(sig, &create, &expiry);

        time_t crtime = create;
        auto   str = rnp_ctime(crtime);
        fprintf(resfp,
                "%s made %s%s",
                title.c_str(),
                rnp_y2k38_warning(crtime) ? ">=" : "",
                str.c_str());
        if (expiry) {
            crtime = rnp_timeadd(crtime, expiry);
            str = rnp_ctime(crtime);
            fprintf(
              resfp, "Valid until %s%s\n", rnp_y2k38_warning(crtime) ? ">=" : "", str.c_str());
        }

        rnp_signature_handle_t handle = NULL;
        if (rnp_op_verify_signature_get_handle(sig, &handle)) {
            ERR_MSG("Failed to obtain signature handle.");
            continue;
        }

        cli_rnp_print_sig_key_info(resfp, handle);
        rnp_key_handle_t key = NULL;

        if ((status != RNP_ERROR_KEY_NOT_FOUND) && !rnp_signature_get_signer(handle, &key)) {
            cli_rnp_print_key_info(resfp, rnp->ffi, key, false, false);
            rnp_key_handle_destroy(key);
        }
        rnp_signature_handle_destroy(handle);
    }

    if (!sigs.size()) {
        ERR_MSG("No signature(s) found - is this a signed file?");
        return;
    }
    if (!invalidc && !unknownc) {
        ERR_MSG("Signature(s) verified successfully");
        return;
    }
    /* Show a proper error message if there are invalid/unknown signatures */
    auto si = invalidc > 1 ? "s" : "";
    auto su = unknownc > 1 ? "s" : "";
    auto fail = "Signature verification failure: ";
    if (invalidc && !unknownc) {
        ERR_MSG("%s%u invalid signature%s", fail, invalidc, si);
    } else if (!invalidc && unknownc) {
        ERR_MSG("%s%u unknown signature%s", fail, unknownc, su);
    } else {
        ERR_MSG("%s%u invalid signature%s, %u unknown signature%s",
                fail,
                invalidc,
                si,
                unknownc,
                su);
    }
}

static void
cli_rnp_inform_of_hidden_recipient(rnp_op_verify_t op)
{
    size_t recipients = 0;
    rnp_op_verify_get_recipient_count(op, &recipients);
    if (!recipients) {
        return;
    }
    for (size_t idx = 0; idx < recipients; idx++) {
        rnp_recipient_handle_t recipient = NULL;
        rnp_op_verify_get_recipient_at(op, idx, &recipient);
        char *keyid = NULL;
        rnp_recipient_get_keyid(recipient, &keyid);
        bool hidden = keyid && !strcmp(keyid, "0000000000000000");
        rnp_buffer_destroy(keyid);
        if (hidden) {
            ERR_MSG("Warning: message has hidden recipient, but it was ignored. Use "
                    "--allow-hidden to override this.");
            break;
        }
    }
}

bool
cli_rnp_process_file(cli_rnp_t *rnp)
{
    rnp_input_t input = NULL;
    if (!rnp->init_io(Operation::Verify, &input, NULL)) {
        ERR_MSG("failed to open source");
        return false;
    }

    char *contents = NULL;
    if (rnp_guess_contents(input, &contents)) {
        ERR_MSG("failed to check source contents");
        rnp_input_destroy(input);
        return false;
    }

    /* source data for detached signature verification */
    rnp_input_t                            source = NULL;
    rnp_output_t                           output = NULL;
    rnp_op_verify_t                        verify = NULL;
    rnp_result_t                           ret = RNP_ERROR_GENERIC;
    bool                                   res = false;
    std::vector<rnp_op_verify_signature_t> sigs;
    size_t                                 scount = 0;

    if (rnp::str_case_eq(contents, "signature")) {
        /* detached signature */
        std::string in = rnp->cfg().get_str(CFG_INFILE);
        std::string src = rnp->cfg().get_str(CFG_SOURCE);
        if (is_stdinout_spec(in) && is_stdinout_spec(src)) {
            ERR_MSG("Detached signature and signed source cannot be both stdin.");
            goto done;
        }
        if (src.empty() && !has_extension(in, EXT_SIG) && !has_extension(in, EXT_ASC)) {
            ERR_MSG("Unsupported detached signature extension. Use --source to override.");
            goto done;
        }
        if (src.empty()) {
            src = in;
            /* cannot fail as we checked for extension previously */
            strip_extension(src);
        }
        source = cli_rnp_input_from_specifier(*rnp, src, NULL);
        if (!source) {
            ERR_MSG("Failed to open source for detached signature verification.");
            goto done;
        }

        ret = rnp_op_verify_detached_create(&verify, rnp->ffi, source, input);
        if (!ret) {
            /* Currently CLI requires all signatures to be valid for success */
            ret = rnp_op_verify_set_flags(verify, RNP_VERIFY_REQUIRE_ALL_SIGS);
        }
    } else {
        if (!rnp->init_io(Operation::Verify, NULL, &output)) {
            ERR_MSG("Failed to create output stream.");
            goto done;
        }
        ret = rnp_op_verify_create(&verify, rnp->ffi, input, output);
        if (!ret) {
            uint32_t flags = 0;
            if (!rnp->cfg().get_bool(CFG_NO_OUTPUT)) {
                /* This would happen if user requested decryption instead of verification */
                flags = flags | RNP_VERIFY_IGNORE_SIGS_ON_DECRYPT;
            } else {
                /* Currently CLI requires all signatures to be valid for success */
                flags = flags | RNP_VERIFY_REQUIRE_ALL_SIGS;
            }
            if (rnp->cfg().get_bool(CFG_ALLOW_HIDDEN)) {
                /* Allow hidden recipient */
                flags = flags | RNP_VERIFY_ALLOW_HIDDEN_RECIPIENT;
            }
            ret = rnp_op_verify_set_flags(verify, flags);
        }
    }
    if (ret) {
        ERR_MSG("Failed to initialize verification/decryption operation.");
        goto done;
    }

    res = !rnp_op_verify_execute(verify);

    /* Check whether we had hidden recipient on verification/decryption failure */
    if (!res && !rnp->cfg().get_bool(CFG_ALLOW_HIDDEN)) {
        cli_rnp_inform_of_hidden_recipient(verify);
    }

    rnp_op_verify_get_signature_count(verify, &scount);
    if (!scount) {
        goto done;
    }

    for (size_t i = 0; i < scount; i++) {
        rnp_op_verify_signature_t sig = NULL;
        if (rnp_op_verify_get_signature_at(verify, i, &sig)) {
            ERR_MSG("Failed to obtain signature info.");
            res = false;
            goto done;
        }
        try {
            sigs.push_back(sig);
        } catch (const std::exception &e) {
            ERR_MSG("%s", e.what());
            res = false;
            goto done;
        }
    }
    cli_rnp_print_signatures(rnp, sigs);
done:
    rnp_buffer_destroy(contents);
    rnp_input_destroy(input);
    rnp_input_destroy(source);
    rnp_output_destroy(output);
    rnp_op_verify_destroy(verify);
    return res;
}

void
cli_rnp_print_praise(void)
{
    printf("%s\n%s\n", PACKAGE_STRING, PACKAGE_BUGREPORT);
    printf("Backend: %s\n", rnp_backend_string());
    printf("Backend version: %s\n", rnp_backend_version());
    printf("Supported algorithms:\n");
    cli_rnp_print_feature(stdout, RNP_FEATURE_PK_ALG, "Public key");
    cli_rnp_print_feature(stdout, RNP_FEATURE_SYMM_ALG, "Encryption");
    cli_rnp_print_feature(stdout, RNP_FEATURE_AEAD_ALG, "AEAD");
    cli_rnp_print_feature(stdout, RNP_FEATURE_PROT_MODE, "Key protection");
    cli_rnp_print_feature(stdout, RNP_FEATURE_HASH_ALG, "Hash");
    cli_rnp_print_feature(stdout, RNP_FEATURE_COMP_ALG, "Compression");
    cli_rnp_print_feature(stdout, RNP_FEATURE_CURVE, "Curves");
    printf("Please report security issues at (https://www.rnpgp.org/feedback) and\n"
           "general bugs at https://github.com/rnpgp/rnp/issues.\n");
}

void
cli_rnp_print_feature(FILE *fp, const char *type, const char *printed_type)
{
    char * result = NULL;
    size_t count;
    if (rnp_supported_features(type, &result) != RNP_SUCCESS) {
        ERR_MSG("Failed to list supported features: %s", type);
        return;
    }
    json_object *jso = json_tokener_parse(result);
    if (!jso) {
        ERR_MSG("Failed to parse JSON with features: %s", type);
        goto done;
    }
    fprintf(fp, "%s: ", printed_type);
    count = json_object_array_length(jso);
    for (size_t idx = 0; idx < count; idx++) {
        json_object *val = json_object_array_get_idx(jso, idx);
        fprintf(fp, " %s%s", json_object_get_string(val), idx < count - 1 ? "," : "");
    }
    fputs("\n", fp);
    fflush(fp);
    json_object_put(jso);
done:
    rnp_buffer_destroy(result);
}
