diff options
Diffstat (limited to 'src/rnp/fficli.cpp')
-rw-r--r-- | src/rnp/fficli.cpp | 3229 |
1 files changed, 3229 insertions, 0 deletions
diff --git a/src/rnp/fficli.cpp b/src/rnp/fficli.cpp new file mode 100644 index 0000000..fa118ee --- /dev/null +++ b/src/rnp/fficli.cpp @@ -0,0 +1,3229 @@ +/* + * 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; + unsigned validc = 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"; + validc++; + 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); +} |