From 8053187731ae8e3eb368d8360989cf5fd6eed9f7 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 05:32:49 +0200 Subject: Adding upstream version 0.17.0. Signed-off-by: Daniel Baumann --- src/rnpkeys/CMakeLists.txt | 101 +++++++ src/rnpkeys/main.cpp | 136 ++++++++++ src/rnpkeys/rnpkeys.1.adoc | 453 +++++++++++++++++++++++++++++++ src/rnpkeys/rnpkeys.cpp | 648 +++++++++++++++++++++++++++++++++++++++++++++ src/rnpkeys/rnpkeys.h | 80 ++++++ src/rnpkeys/tui.cpp | 413 +++++++++++++++++++++++++++++ 6 files changed, 1831 insertions(+) create mode 100644 src/rnpkeys/CMakeLists.txt create mode 100644 src/rnpkeys/main.cpp create mode 100644 src/rnpkeys/rnpkeys.1.adoc create mode 100644 src/rnpkeys/rnpkeys.cpp create mode 100644 src/rnpkeys/rnpkeys.h create mode 100644 src/rnpkeys/tui.cpp (limited to 'src/rnpkeys') diff --git a/src/rnpkeys/CMakeLists.txt b/src/rnpkeys/CMakeLists.txt new file mode 100644 index 0000000..6302903 --- /dev/null +++ b/src/rnpkeys/CMakeLists.txt @@ -0,0 +1,101 @@ +# Copyright (c) 2018-2020 Ribose Inc. +# 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 HOLDERS 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. + +if(MSVC) + # remove extra ${Configuration} subfolder + set(ArchiveOutputDir ${CMAKE_BINARY_DIR}\\src\\rnpkeys) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${ArchiveOutputDir}) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_MINSIZEREL ${ArchiveOutputDir}) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${ArchiveOutputDir}) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO ${ArchiveOutputDir}) + + set(RuntimeOutputDir ${CMAKE_BINARY_DIR}\\src\\rnpkeys) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${RuntimeOutputDir}) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL ${RuntimeOutputDir}) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${RuntimeOutputDir}) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO ${RuntimeOutputDir}) + + find_path(GETOPT_INCLUDE_DIR + NAMES getopt.h + ) + find_library(GETOPT_LIBRARY + NAMES getopt + ) + find_path(DIRENT_INCLUDE_DIR + NAMES dirent.h + ) +endif() + +# for the headers +find_package(JSON-C 0.11 REQUIRED) + +add_executable(rnpkeys + rnpkeys.cpp + tui.cpp + main.cpp + ../rnp/rnpcfg.cpp + ../rnp/fficli.cpp +) + +if(BUILD_SHARED_LIBS) + target_sources(rnpkeys PRIVATE ../lib/logging.cpp $) +endif(BUILD_SHARED_LIBS) + +target_include_directories(rnpkeys + PRIVATE + "${PROJECT_SOURCE_DIR}/src" + "${PROJECT_SOURCE_DIR}/src/lib" + "${JSON-C_INCLUDE_DIRS}" +) +if(MSVC) + target_include_directories(rnpkeys + PRIVATE + "${GETOPT_INCLUDE_DIR}" + "${DIRENT_INCLUDE_DIR}" + ) +endif() + +target_link_libraries(rnpkeys + PRIVATE + librnp + JSON-C::JSON-C +) +if(MSVC) + target_link_libraries(rnpkeys + PRIVATE + "${GETOPT_LIBRARY}" + ) +endif() + +include(GNUInstallDirs) +install(TARGETS rnpkeys + RUNTIME + DESTINATION "${CMAKE_INSTALL_BINDIR}" + COMPONENT cli +) + +# Build and install man page +if (ENABLE_DOC) + add_adoc_man("${CMAKE_CURRENT_SOURCE_DIR}/rnpkeys.1.adoc" ${RNP_VERSION}) +endif() diff --git a/src/rnpkeys/main.cpp b/src/rnpkeys/main.cpp new file mode 100644 index 0000000..8bcb7e1 --- /dev/null +++ b/src/rnpkeys/main.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2017, [Ribose Inc](https://www.ribose.com). + * Copyright (c) 2009 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is originally derived from software contributed to + * The NetBSD Foundation by Alistair Crooks (agc@netbsd.org), and + * carried further by Ribose Inc (https://www.ribose.com). + * + * 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 HOLDERS 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. + */ +/* Command line program to perform rnp operations */ +#ifdef _MSC_VER +#include "uniwin.h" +#else +#include +#endif +#include +#include +#include "rnpkeys.h" + +extern struct option options[]; +extern const char * usage; + +optdefs_t +get_short_cmd(int ch) +{ + switch (ch) { + case 'V': + return CMD_VERSION; + case 'g': + return CMD_GENERATE_KEY; + case 'l': + return CMD_LIST_KEYS; + case 'h': + [[fallthrough]]; + default: + return CMD_HELP; + } +} + +#ifndef RNP_RUN_TESTS +int +main(int argc, char **argv) +#else +int rnpkeys_main(int argc, char **argv); +int +rnpkeys_main(int argc, char **argv) +#endif +{ + if (argc < 2) { + print_usage(usage); + return EXIT_FAILURE; + } + + cli_rnp_t rnp = {}; +#if !defined(RNP_RUN_TESTS) && defined(_WIN32) + try { + rnp.substitute_args(&argc, &argv); + } catch (std::exception &ex) { + RNP_LOG("Error converting arguments ('%s')", ex.what()); + return EXIT_FAILURE; + } +#endif + + rnp_cfg cfg; + optdefs_t cmd = CMD_NONE; + int optindex = 0; + int ch; + + while ((ch = getopt_long(argc, argv, "Vglh", options, &optindex)) != -1) { + /* Check for unsupported command/option */ + if (ch == '?') { + print_usage(usage); + return EXIT_FAILURE; + } + + optdefs_t newcmd = cmd; + if (ch >= CMD_LIST_KEYS) { + if (!setoption(cfg, &newcmd, options[optindex].val, optarg)) { + ERR_MSG("Failed to process argument --%s", options[optindex].name); + return EXIT_FAILURE; + } + } else { + newcmd = get_short_cmd(ch); + } + + if (cmd && newcmd != cmd) { + ERR_MSG("Conflicting commands!"); + return EXIT_FAILURE; + } + cmd = newcmd; + } + + /* No initialization required for these two commands. */ + if (cmd == CMD_HELP || cmd == CMD_VERSION) { + return cli_rnp_t::ret_code(rnp_cmd(&rnp, cmd, NULL)); + } + + if (!rnpkeys_init(rnp, cfg)) { + return EXIT_FAILURE; + } + + if (!cli_rnp_setup(&rnp)) { + return EXIT_FAILURE; + } + + /* now do the required action for each of the command line args */ + if (optind == argc) { + return cli_rnp_t::ret_code(rnp_cmd(&rnp, cmd, NULL)); + } + bool success = true; + for (int i = optind; i < argc; i++) { + success = success && rnp_cmd(&rnp, cmd, argv[i]); + } + return cli_rnp_t::ret_code(success); +} diff --git a/src/rnpkeys/rnpkeys.1.adoc b/src/rnpkeys/rnpkeys.1.adoc new file mode 100644 index 0000000..2b09d17 --- /dev/null +++ b/src/rnpkeys/rnpkeys.1.adoc @@ -0,0 +1,453 @@ += rnpkeys(1) +RNP +:doctype: manpage +:release-version: {component-version} +:man manual: RNP Manual +:man source: RNP {release-version} + +== NAME + +RNPKEYS - OpenPGP key management utility. + +== SYNOPSIS + +*rnpkeys* [_--homedir_ _dir_] [_OPTIONS_] _COMMAND_ + +== DESCRIPTION + +The _rnpkeys_ command-line utility is part of the _RNP_ suite and +provides OpenPGP key management functionality, including: + +* key listing; +* key generation; +* key import/export; and +* key editing. + + +=== BASICS + +By default, *rnp* will apply a _COMMAND_, additionally configured with _OPTIONS_, +to all _INPUT_FILE_(s) or _stdin_ if no _INPUT_FILE_ is given. +There are some special cases for _INPUT_FILE_ : + +* _-_ (dash) substitutes to _stdin_ +* env:VARIABLE_NAME substitutes to the contents of environment variable VARIABLE_NAME + +Depending on the input, output may be written: + +* to the specified file with a removed or added file extension (_.pgp_, _.asc_, _.sig_); or +* to _stdout_. + +Without the *--armor* option, output will be in binary. + +If _COMMAND_ requires public or private keys, *rnp* will look for the keyrings in *~/.rnp*. The options *--homedir* and *--keyfile* override this (see below). + +If _COMMAND_ needs a password, *rnp* will ask for it via *stdin* or *tty*, +unless the *--password* or *--pass-fd* option was specified. + + +By default, *rnpkeys* will use keyrings stored in the _~/.rnp_ directory. + +This behavior may be overridden with the _--homedir_ option. + +If _COMMAND_ needs a password, the command will prompt the caller +via _stdin_ or _tty_, unless the *--password* or *--pass-fd* +options were also used. + +=== SPECIFYING KEYS + +Most *rnpkeys* commands require a key locator or a filter, +representing one or more keys. + +It may be specified in one of the following ways: + +*userid*:: +Or just part of the *userid*. +For *"Alice "*, the following methods are considered identical: + +** _alice_ +** _alice@rnpgp_ +** _rnpgp.com_ + +*keyid*:: +Or its right-most 8 characters. With or without _0x_ at the beginning and spaces/tabs inside. Such as: + +** _0x725F6F2D6D5F6120_ +** _"725F6F2D 6D5F6120"_ +** _0x6D5F6120_ + +*key fingerprint*: The 40-character key fingerprint, such as: + +** _"0x416E746F 6E537669 72696465 6E6B6F20"_ + + + +== COMMANDS + +=== INFORMATIONAL + +*-h*, *--help*:: +Displays a short help message. No options are expected. + +*-V*, *--version*:: +Displays version information. No options are expected. + +*-l*, *--list-keys*:: +List out keys and some brief information about each. + ++ +Additional options: + +*--with-sigs*::: +Additionally display signatures of listed keys. + + +=== KEY GENERATION + +*-g*, *--generate-key*:: +Generate a new keypair. + ++ +Without additional options, an RSA primary key pair with an RSA sub-key pair will be generated, and prompting for the encryption password afterwards. ++ +Additional options: + +*--numbits*::: +Overrides the default RSA key size of *2048* bits. + +*--expiration* _TIME_::: +Set key and subkey expiration time, counting from the creation time. + ++ +By default generated keys do not expire. + ++ +Expiration time can be specified as: + +* expiration date in the ISO 8601:2019 date format (_yyyy-mm-dd_); or +* hours/days/months/years since creation time with the syntax of _20h_/_30d_/_1m_/_1y_; +* number of seconds. + +*--expert*::: +Select key algorithms interactively and override default settings. + +*--userid*::: +Specifies the _userid_ to be used in generation. + +*--hash*::: +Specify the hash algorithm used in generation. + +*--cipher*::: +Specify the encryption algorithm used in generation. + +*--s2k-iterations*::: +Specify the number of iterations for the S2K (string-to-key) process. + ++ +This is used during the derivation of the symmetric key, which +encrypts a secret key from the password. + + +*--s2k-msec*::: +Specify that *rnpkeys* should automatically pick a +*--s2k-iterations* value such that the single key derivation operation +would take _NUMBER_ of milliseconds on the current system. + ++ +For example, setting it to _2000_ would mean that each secret key +decryption operation would take around 2 seconds (on the current machine). + + +=== KEY/SIGNATURE IMPORT + +*--import*, *--import-keys*, *--import-sigs*:: +Import keys or signatures. + ++ +While *rnpkeys* automatically detects the input data format, +one may still wish to specify whether the input provides keys or signatures. + ++ +By default, the import process will stop on the first discovered +erroneous key or signature. + ++ +Additional options: + +*--permissive*::: +Skip errored or unsupported packets during the import process. + +=== KEY/SIGNATURE EXPORT + +*--export-key* [*--userid*=_FILTER_] [_FILTER_]:: +Export key(s). Only export keys that match _FILTER_ if _FILTER_ is given. + ++ +If filter matches a primary key, the subkeys of the primary key are also exported. ++ +By default, key data is written to _stdout_ in ASCII-armored format. ++ +Additional options: + +*--output* _PATH_::: +Specifies output to be written to a file name instead of _stdout_. + +*--secret*::: +Without this option specified, the command will only export public key(s). +This option must be provided to export secret key(s). + +*--export-rev* _KEY_:: +Export the revocation signature for a specified secret key. + ++ +The revocation signature can be used later in a case of key loss or compromise. ++ +Additional options: + +*--rev-type*::: +Specifies type of key revocation. + +*--rev-reason*::: +Specifies reason for key revocation. + + +=== KEY MANIPULATION + +*--revoke-key* _KEY_:: +Issue revocation signature for the secret key, and save it in the keyring. + ++ +Revoked keys cannot be used further. + ++ +Additional options: + +*--rev-type*::: +Specifies type of key revocation, see *options* section for the available values. + +*--rev-reason*::: +Specifies reason for key revocation. + + +*--remove-key* _KEY_:: +Remove the specified key. + ++ +If a primary key is specified, then all of its subkeys are also removed. + ++ +If the specified key is a secret key, then it will not be deleted without +confirmation. ++ +Additional options: + +*--force*::: +Forces removal of a secret key without prompting the user. + +*--edit-key* _KEY_:: +Edit or update information, associated with a key. Should be accompanied with editing option. + ++ +Currently the following options are available: + ++ +*--add-subkey*::: +Generate and add a new subkey to the existing primary key. All additional options for the +*--generate-key* command apply for subkey generation as well, except *--userid*. + +*--check-cv25519-bits*::: +Check whether least significant/most significant bits of Curve25519 ECDH subkey are correctly set. +RNP internally sets those bits to required values (3 least significant bits and most significant bit must be zero) during decryption, +however other implementations (GnuPG) may require those bits to be set in key material. +_KEY_ must specify the exact subkey via keyid or fingerprint. + +*--fix-cv25519-bits*::: +Set least significant/most significant bits of Curve25519 ECDH subkey to the correct values, and save a key. +So later export of the key would ensure compatibility with other implementations (like GnuPG). +This operation would require the password for your secret key. +Since version 0.16.0 of RNP generated secret key is stored with bits set to a needed value, +however, this may be needed to fix older keys or keys generated by other implementations. +_KEY_ must specify the exact subkey via keyid or fingerprint. + +*--set-expire* _TIME_::: +Set key expiration time. See the description of the *--expiration* option for possible time formats. +Setting argument to 0 removes key expiration, the key would never expire. It is not recommended +due to security reasons. + +=== OPTIONS + +*--homedir* _DIR_:: +Change homedir (where RNP looks for keyrings) to the specified value. + ++ +The default homedir is _~/.rnp_ . + +*--output* _PATH_:: +Write data processing related output to the file specified. + ++ +Combine it with *--overwrite* to overwrite file if it already exists. + +*--overwrite*:: +Overwrite output file if it already exists. + ++ + +*--userid* _USERID_:: +Use the specified _userid_ during key generation and in some +key-searching operations. + +*--numbits* _BITS_:: +Specify size in bits for the generated key and subkey. + ++ +_bits_ may be in range *1024*-*16384*, as long as the public key algorithm +does not place additional limits. + +*--cipher* _ALGORITHM_:: +Set the key encryption algorithm. This is only used in key generation. + ++ +The default value is _AES256_. + +*--hash* _ALGORITHM_:: +Use the specified hash algorithm for signatures and derivation of the encrypting key from password for secret key encryption. + ++ +The default value is _SHA256_. + +*--expert*:: +Use the *expert key generation* mode, allowing the selection of +key/subkey algorithms. + ++ +The following types of keys can be generated in this mode: + ++ +-- +** *DSA* key with *ElGamal* encryption subkey +** *DSA* key with *RSA* subkey +** *ECDSA* key with *ECDH* subkey +** *EdDSA* key with *x25519* subkey +** *SM2* key with subkey +-- ++ +Specifically, for *ECDSA* and *ECDH* the underlying curve can also be specified: + ++ +-- +** _NIST P-256_, _NIST P-384_, _NIST P-521_ +** _brainpoolP256r1_, _brainpoolP384r1_, _brainpoolP512r1_ +** _secp256k1_ +-- + +*--pass-fd* _FD_:: +Specify a file descriptor to read passwords from instead of from _stdin_/_tty_. + ++ +Useful for automated or non-interactive sessions. + +*--password* _PASSWORD_:: +Use the specified password when it is needed. + ++ +WARNING: Not recommended for production use due to potential security issues. +Use *--pass-fd* for batch operations instead. + +*--with-sigs*:: +Print signature information when listing keys via the *-l* command. + +*--force*:: +Force actions to happen without prompting the user. + ++ +This applies to cases such as secret key removal, revoking an already revoked key and so on. + +*--permissive*:: +Skip malformed or unknown keys/signatures during key import. + ++ +By default, *rnpkeys* will stop on the first erroring packet +and exit with an error. + +*--rev-type* _TYPE_:: +Use the specified type during revocation signature generation instead of the default _0_. + ++ +The following values are supported: + ++ +-- +** 0, or "no": no revocation type specified. +** 1, or "superseded": key was superseded with another key. +** 2, or "compromised": key was compromised and no longer valid. +** 3, or "retired": key is retired. +-- ++ +Please refer to *IETF RFC 4880* for details. + +*--rev-reason* _REASON_:: +Add the specified human-readable revocation _REASON_ to the +signature instead of an empty string. + +*--s2k-iterations* _NUMBER_:: +Specify the number of iterations for the S2K (string-to-key) process. + ++ +This is used during the derivation of the symmetric key, which +encrypts a secret key from the password. + ++ +Please refer to IETF RFC 4880 for further details. + +*--s2k-msec* _NUMBER_:: +Specify that *rnpkeys* should automatically pick a +*--s2k-iterations* value such that the single key derivation operation +would take _NUMBER_ of milliseconds on the current system. + ++ +For example, setting it to _2000_ would mean that each secret key +decryption operation would take around 2 seconds (on the current machine). + +*--notty*:: +Disable use of tty. + ++ +By default RNP would detect whether TTY is attached and use it for user prompts. + ++ +This option overrides default behaviour so user input may be passed in batch mode. + +*--current-time* _TIME_:: +Override system's time with a specified value. + ++ +By default RNP uses system's time in all signature/key checks, however in some scenarios it could be needed to override this. + ++ +*TIME* could be specified in the ISO 8601-1:2019 date format (_yyyy-mm-dd_), or in the UNIX timestamp format. + +== EXIT STATUS + +_0_:: + Success. + +_Non-zero_:: + Failure. + +== EXAMPLES + +The following examples demonstrate method of usage of the _rnpkeys_ command. + +=== EXAMPLE 1: IMPORT EXISTING KEYS FROM THE GNUPG + +Following oneliner may be used to import all public keys from the GnuPG: + +*gpg* *-a* *--export* | *rnpkeys* *--import* _-_ + +To import all secret keys the following command should be used (please note, that you'll be asked for secret key password(s)): + +*gpg* *-a* *--export-secret-keys* | *rnpkeys* *--import* _-_ + +=== EXAMPLE 2: GENERATE A NEW KEY + +This example generates a new key with specified userid and expiration. +Also it enables "expert" mode, allowing the selection of key/subkey algorithms. + +*rnpkeys* *--generate* *--userid* *"john@doe.com"* *--expert* *--expiration* *1y* + +== BUGS + +Please report _issues_ via the RNP public issue tracker at: +https://github.com/rnpgp/rnp/issues. + +_Security reports_ or _security-sensitive feedback_ should be reported +according to the instructions at: +https://www.rnpgp.org/feedback. + + +== AUTHORS + +*RNP* is an open source project led by Ribose and has +received contributions from numerous individuals and +organizations. + + +== RESOURCES + +*Web site*: https://www.rnpgp.org + +*Source repository*: https://github.com/rnpgp/rnp + + +== COPYING + +Copyright \(C) 2017-2021 Ribose. +The RNP software suite is _freely licensed_: +please refer to the *LICENSE* file for details. + + + +== SEE ALSO + +*rnp(1)*, *librnp(3)* diff --git a/src/rnpkeys/rnpkeys.cpp b/src/rnpkeys/rnpkeys.cpp new file mode 100644 index 0000000..1a6997c --- /dev/null +++ b/src/rnpkeys/rnpkeys.cpp @@ -0,0 +1,648 @@ +/* + * Copyright (c) 2017-2021, [Ribose Inc](https://www.ribose.com). + * Copyright (c) 2009 The NetBSD Foundation, Inc. + * All rights reserved. + * + * This code is originally derived from software contributed to + * The NetBSD Foundation by Alistair Crooks (agc@netbsd.org), and + * carried further by Ribose Inc (https://www.ribose.com). + * + * 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 HOLDERS 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. + */ +/* Command line program to perform rnp operations */ + +#ifdef _MSC_VER +#include "uniwin.h" +#else +#include +#endif +#include +#include +#include "rnpkeys.h" +#include "str-utils.h" +#include + +const char *usage = + "Manipulate OpenPGP keys and keyrings.\n" + "Usage: rnpkeys --command [options] [files]\n" + "Commands:\n" + " -h, --help This help message.\n" + " -V, --version Print RNP version information.\n" + " -g, --generate-key Generate a new keypair (default is RSA).\n" + " --userid Specify key's userid.\n" + " --expert Select key type, size, and additional parameters.\n" + " --numbits Override default key size (2048).\n" + " --expiration Set key and subkey expiration time.\n" + " --cipher Set cipher used to encrypt a secret key.\n" + " --hash Set hash which is used for key derivation.\n" + " --allow-weak-hash Allow usage of a weak hash algorithm.\n" + " -l, --list-keys List keys in the keyrings.\n" + " --secret List secret keys instead of public ones.\n" + " --with-sigs List signatures as well.\n" + " --import Import keys or signatures.\n" + " --import-keys Import keys.\n" + " --import-sigs Import signatures.\n" + " --permissive Skip erroring keys/sigs instead of failing.\n" + " --export-key Export a key.\n" + " --secret Export a secret key instead of a public.\n" + " --export-rev Export a key's revocation.\n" + " --rev-type Set revocation type.\n" + " --rev-reason Human-readable reason for revocation.\n" + " --revoke-key Revoke a key specified.\n" + " --remove-key Remove a key specified.\n" + " --edit-key Edit key properties.\n" + " --add-subkey Add new subkey.\n" + " --check-cv25519-bits Check whether Cv25519 subkey bits are correct.\n" + " --fix-cv25519-bits Fix Cv25519 subkey bits.\n" + " --set-expire Set key expiration time.\n" + "\n" + "Other options:\n" + " --homedir Override home directory (default is ~/.rnp/).\n" + " --password Password, which should be used during operation.\n" + " --pass-fd Read password(s) from the file descriptor.\n" + " --force Force operation (like secret key removal).\n" + " --output [file, -] Write data to the specified file or stdout.\n" + " --overwrite Overwrite output file without a prompt.\n" + " --notty Do not write anything to the TTY.\n" + " --current-time Override system's time.\n" + "\n" + "See man page for a detailed listing and explanation.\n" + "\n"; + +struct option options[] = { + /* key-management commands */ + {"list-keys", no_argument, NULL, CMD_LIST_KEYS}, + {"export", no_argument, NULL, CMD_EXPORT_KEY}, + {"export-key", optional_argument, NULL, CMD_EXPORT_KEY}, + {"import", no_argument, NULL, CMD_IMPORT}, + {"import-key", no_argument, NULL, CMD_IMPORT_KEYS}, + {"import-keys", no_argument, NULL, CMD_IMPORT_KEYS}, + {"import-sigs", no_argument, NULL, CMD_IMPORT_SIGS}, + {"gen", optional_argument, NULL, CMD_GENERATE_KEY}, + {"gen-key", optional_argument, NULL, CMD_GENERATE_KEY}, + {"generate", optional_argument, NULL, CMD_GENERATE_KEY}, + {"generate-key", optional_argument, NULL, CMD_GENERATE_KEY}, + {"export-rev", no_argument, NULL, CMD_EXPORT_REV}, + {"export-revocation", no_argument, NULL, CMD_EXPORT_REV}, + {"revoke-key", no_argument, NULL, CMD_REVOKE_KEY}, + {"remove-key", no_argument, NULL, CMD_REMOVE_KEY}, + {"edit-key", no_argument, NULL, CMD_EDIT_KEY}, + /* debugging commands */ + {"help", no_argument, NULL, CMD_HELP}, + {"version", no_argument, NULL, CMD_VERSION}, + {"debug", required_argument, NULL, OPT_DEBUG}, + /* options */ + {"coredumps", no_argument, NULL, OPT_COREDUMPS}, + {"keystore-format", required_argument, NULL, OPT_KEY_STORE_FORMAT}, + {"userid", required_argument, NULL, OPT_USERID}, + {"with-sigs", no_argument, NULL, OPT_WITH_SIGS}, + {"hash", required_argument, NULL, OPT_HASH_ALG}, + {"home", required_argument, NULL, OPT_HOMEDIR}, + {"homedir", required_argument, NULL, OPT_HOMEDIR}, + {"numbits", required_argument, NULL, OPT_NUMBITS}, + {"s2k-iterations", required_argument, NULL, OPT_S2K_ITER}, + {"s2k-msec", required_argument, NULL, OPT_S2K_MSEC}, + {"expiration", required_argument, NULL, OPT_EXPIRATION}, + {"pass-fd", required_argument, NULL, OPT_PASSWDFD}, + {"password", required_argument, NULL, OPT_PASSWD}, + {"results", required_argument, NULL, OPT_RESULTS}, + {"cipher", required_argument, NULL, OPT_CIPHER}, + {"expert", no_argument, NULL, OPT_EXPERT}, + {"output", required_argument, NULL, OPT_OUTPUT}, + {"overwrite", no_argument, NULL, OPT_OVERWRITE}, + {"force", no_argument, NULL, OPT_FORCE}, + {"secret", no_argument, NULL, OPT_SECRET}, + {"rev-type", required_argument, NULL, OPT_REV_TYPE}, + {"rev-reason", required_argument, NULL, OPT_REV_REASON}, + {"permissive", no_argument, NULL, OPT_PERMISSIVE}, + {"notty", no_argument, NULL, OPT_NOTTY}, + {"fix-cv25519-bits", no_argument, NULL, OPT_FIX_25519_BITS}, + {"check-cv25519-bits", no_argument, NULL, OPT_CHK_25519_BITS}, + {"add-subkey", no_argument, NULL, OPT_ADD_SUBKEY}, + {"set-expire", required_argument, NULL, OPT_SET_EXPIRE}, + {"current-time", required_argument, NULL, OPT_CURTIME}, + {"allow-weak-hash", no_argument, NULL, OPT_ALLOW_WEAK_HASH}, + {NULL, 0, NULL, 0}, +}; + +/* list keys */ +static bool +print_keys_info(cli_rnp_t *rnp, FILE *fp, const char *filter) +{ + bool psecret = rnp->cfg().get_bool(CFG_SECRET); + bool psigs = rnp->cfg().get_bool(CFG_WITH_SIGS); + int flags = CLI_SEARCH_SUBKEYS_AFTER | (psecret ? CLI_SEARCH_SECRET : 0); + std::vector keys; + + if (!cli_rnp_keys_matching_string(rnp, keys, filter ? filter : "", flags)) { + fprintf(fp, "Key(s) not found.\n"); + return false; + } + fprintf(fp, "%d key%s found\n", (int) keys.size(), (keys.size() == 1) ? "" : "s"); + for (auto key : keys) { + cli_rnp_print_key_info(fp, rnp->ffi, key, psecret, psigs); + } + + fprintf(fp, "\n"); + /* clean up */ + clear_key_handles(keys); + return true; +} + +static bool +import_keys(cli_rnp_t *rnp, rnp_input_t input, const std::string &inname) +{ + std::set new_pub_keys; + std::set new_sec_keys; + std::set updated_keys; + bool res = false; + bool updated = false; + size_t unchanged_keys = 0; + size_t processed_keys = 0; + + uint32_t flags = RNP_LOAD_SAVE_PUBLIC_KEYS | RNP_LOAD_SAVE_SECRET_KEYS | + RNP_LOAD_SAVE_SINGLE | RNP_LOAD_SAVE_BASE64; + + bool permissive = rnp->cfg().get_bool(CFG_PERMISSIVE); + if (permissive) { + flags |= RNP_LOAD_SAVE_PERMISSIVE; + } + + do { + /* load keys one-by-one */ + char * results = NULL; + rnp_result_t ret = rnp_import_keys(rnp->ffi, input, flags, &results); + if (ret == RNP_ERROR_EOF) { + res = true; + break; + } + if (ret && updated) { + /* some keys were imported, but then error occurred */ + ERR_MSG("warning: not all data was processed."); + res = true; + break; + } + if (ret) { + ERR_MSG("failed to import key(s) from %s, stopping.", inname.c_str()); + break; + } + + // print information about imported key(s) + json_object *jso = json_tokener_parse(results); + rnp_buffer_destroy(results); + if (!jso) { + ERR_MSG("invalid key import resulting JSON"); + break; + } + json_object *keys = NULL; + if (!json_object_object_get_ex(jso, "keys", &keys)) { + ERR_MSG("invalid key import JSON contents"); + json_object_put(jso); + break; + } + processed_keys += json_object_array_length(keys); + for (size_t idx = 0; idx < (size_t) json_object_array_length(keys); idx++) { + json_object * keyinfo = json_object_array_get_idx(keys, idx); + rnp_key_handle_t key = NULL; + if (!keyinfo) { + continue; + } + std::string pub_status = json_obj_get_str(keyinfo, "public"); + std::string sec_status = json_obj_get_str(keyinfo, "secret"); + const char *fphex = json_obj_get_str(keyinfo, "fingerprint"); + + if (pub_status == "new") { + new_pub_keys.insert(fphex); + updated = true; + } + if (sec_status == "new") { + new_sec_keys.insert(fphex); + updated = true; + } + if (pub_status == "updated" || sec_status == "updated") { + updated_keys.insert(fphex); + updated = true; + } + if (pub_status == "unchanged" || sec_status == "unchanged") { + if (!new_pub_keys.count(fphex) && !new_sec_keys.count(fphex) && + !updated_keys.count(fphex)) { + unchanged_keys++; + continue; + } + } + if (rnp_locate_key(rnp->ffi, "fingerprint", fphex, &key) || !key) { + ERR_MSG("failed to locate key with fingerprint %s", fphex); + continue; + } + cli_rnp_print_key_info(stdout, rnp->ffi, key, true, false); + rnp_key_handle_destroy(key); + } + json_object_put(jso); + } while (1); + + // print statistics + ERR_MSG("Import finished: %lu key%s processed, %lu new public keys, %lu new secret keys, " + "%lu updated, %lu unchanged.", + processed_keys, + (processed_keys != 1) ? "s" : "", + new_pub_keys.size(), + new_sec_keys.size(), + updated_keys.size(), + unchanged_keys); + + if (updated) { + // set default key if we didn't have one + if (rnp->defkey().empty()) { + rnp->set_defkey(); + } + + // save public and secret keyrings + if (!cli_rnp_save_keyrings(rnp)) { + ERR_MSG("failed to save keyrings"); + } + } + return res; +} + +static bool +import_sigs(cli_rnp_t *rnp, rnp_input_t input, const std::string &inname) +{ + bool res = false; + char * results = NULL; + json_object *jso = NULL; + json_object *sigs = NULL; + int unknown_sigs = 0; + int new_sigs = 0; + int old_sigs = 0; + + if (rnp_import_signatures(rnp->ffi, input, 0, &results)) { + ERR_MSG("Failed to import signatures from %s", inname.c_str()); + goto done; + } + // print information about imported signature(s) + jso = json_tokener_parse(results); + if (!jso || !json_object_object_get_ex(jso, "sigs", &sigs)) { + ERR_MSG("Invalid signature import result"); + goto done; + } + + for (size_t idx = 0; idx < (size_t) json_object_array_length(sigs); idx++) { + json_object *siginfo = json_object_array_get_idx(sigs, idx); + if (!siginfo) { + continue; + } + const char *status = json_obj_get_str(siginfo, "public"); + std::string pub_status = status ? status : "unknown"; + status = json_obj_get_str(siginfo, "secret"); + std::string sec_status = status ? status : "unknown"; + + if ((pub_status == "new") || (sec_status == "new")) { + new_sigs++; + } else if ((pub_status == "unchanged") || (sec_status == "unchanged")) { + old_sigs++; + } else { + unknown_sigs++; + } + } + + // print status information + ERR_MSG("Import finished: %d new signature%s, %d unchanged, %d unknown.", + new_sigs, + (new_sigs != 1) ? "s" : "", + old_sigs, + unknown_sigs); + + // save public and secret keyrings + if ((new_sigs > 0) && !cli_rnp_save_keyrings(rnp)) { + ERR_MSG("Failed to save keyrings"); + goto done; + } + res = true; +done: + json_object_put(jso); + rnp_buffer_destroy(results); + return res; +} + +static bool +import(cli_rnp_t *rnp, const std::string &spec, int cmd) +{ + if (spec.empty()) { + ERR_MSG("Import path isn't specified"); + return false; + } + rnp_input_t input = cli_rnp_input_from_specifier(*rnp, spec, NULL); + if (!input) { + ERR_MSG("Failed to create input for %s", spec.c_str()); + return false; + } + if (cmd == CMD_IMPORT) { + char *contents = NULL; + if (rnp_guess_contents(input, &contents)) { + ERR_MSG("Warning! Failed to guess content type to import. Assuming keys."); + } + cmd = (contents && !strcmp(contents, "signature")) ? CMD_IMPORT_SIGS : CMD_IMPORT_KEYS; + rnp_buffer_destroy(contents); + } + + bool res = false; + switch (cmd) { + case CMD_IMPORT_KEYS: + res = import_keys(rnp, input, spec); + break; + case CMD_IMPORT_SIGS: + res = import_sigs(rnp, input, spec); + break; + default: + ERR_MSG("Unexpected command: %d", cmd); + } + rnp_input_destroy(input); + return res; +} + +/* print a usage message */ +void +print_usage(const char *usagemsg) +{ + cli_rnp_print_praise(); + puts(usagemsg); +} + +/* do a command once for a specified file 'f' */ +bool +rnp_cmd(cli_rnp_t *rnp, optdefs_t cmd, const char *f) +{ + std::string fs; + + switch (cmd) { + case CMD_LIST_KEYS: + if (!f && rnp->cfg().get_count(CFG_USERID)) { + fs = rnp->cfg().get_str(CFG_USERID, 0); + f = fs.c_str(); + } + return print_keys_info(rnp, stdout, f); + case CMD_EXPORT_KEY: { + if (!f && rnp->cfg().get_count(CFG_USERID)) { + fs = rnp->cfg().get_str(CFG_USERID, 0); + f = fs.c_str(); + } + if (!f) { + ERR_MSG("No key specified."); + return 0; + } + return cli_rnp_export_keys(rnp, f); + } + case CMD_IMPORT: + case CMD_IMPORT_KEYS: + case CMD_IMPORT_SIGS: + return import(rnp, f ? f : "", cmd); + case CMD_GENERATE_KEY: { + if (!f) { + size_t count = rnp->cfg().get_count(CFG_USERID); + if (count == 1) { + fs = rnp->cfg().get_str(CFG_USERID, 0); + f = fs.c_str(); + } else if (count > 1) { + ERR_MSG("Only single userid is supported for generated keys"); + return false; + } + } + return cli_rnp_generate_key(rnp, f); + } + case CMD_EXPORT_REV: { + if (!f) { + ERR_MSG("You need to specify key to generate revocation for."); + return false; + } + return cli_rnp_export_revocation(rnp, f); + } + case CMD_REVOKE_KEY: { + if (!f) { + ERR_MSG("You need to specify key or subkey to revoke."); + return false; + } + return cli_rnp_revoke_key(rnp, f); + } + case CMD_REMOVE_KEY: { + if (!f) { + ERR_MSG("You need to specify key or subkey to remove."); + return false; + } + return cli_rnp_remove_key(rnp, f); + } + case CMD_EDIT_KEY: { + if (!f) { + ERR_MSG("You need to specify a key or subkey to edit."); + return false; + } + return rnp->edit_key(f); + } + case CMD_VERSION: + cli_rnp_print_praise(); + return true; + case CMD_HELP: + default: + print_usage(usage); + return true; + } +} + +/* set the option */ +bool +setoption(rnp_cfg &cfg, optdefs_t *cmd, int val, const char *arg) +{ + switch (val) { + case OPT_COREDUMPS: +#ifdef _WIN32 + ERR_MSG("warning: --coredumps doesn't make sense on windows systems."); +#endif + cfg.set_bool(CFG_COREDUMPS, true); + return true; + case CMD_GENERATE_KEY: + cfg.set_bool(CFG_NEEDSSECKEY, true); + *cmd = (optdefs_t) val; + return true; + case OPT_EXPERT: + cfg.set_bool(CFG_EXPERT, true); + return true; + case CMD_LIST_KEYS: + case CMD_EXPORT_KEY: + case CMD_EXPORT_REV: + case CMD_REVOKE_KEY: + case CMD_REMOVE_KEY: + case CMD_EDIT_KEY: + case CMD_IMPORT: + case CMD_IMPORT_KEYS: + case CMD_IMPORT_SIGS: + case CMD_HELP: + case CMD_VERSION: + *cmd = (optdefs_t) val; + return true; + /* options */ + case OPT_KEY_STORE_FORMAT: + cfg.set_str(CFG_KEYSTOREFMT, arg); + return true; + case OPT_USERID: + cfg.add_str(CFG_USERID, arg); + return true; + case OPT_HOMEDIR: + cfg.set_str(CFG_HOMEDIR, arg); + return true; + case OPT_NUMBITS: { + int bits = 0; + if (!rnp::str_to_int(arg, bits) || (bits < 1024) || (bits > 16384)) { + ERR_MSG("wrong bits value: %s", arg); + return false; + } + cfg.set_int(CFG_NUMBITS, bits); + return true; + } + case OPT_ALLOW_WEAK_HASH: + cfg.set_bool(CFG_WEAK_HASH, true); + return true; + case OPT_HASH_ALG: + return cli_rnp_set_hash(cfg, arg); + case OPT_S2K_ITER: { + int iterations = atoi(arg); + if (!iterations) { + ERR_MSG("Wrong iterations value: %s", arg); + return false; + } + cfg.set_int(CFG_S2K_ITER, iterations); + return true; + } + case OPT_EXPIRATION: + cfg.set_str(CFG_KG_PRIMARY_EXPIRATION, arg); + cfg.set_str(CFG_KG_SUBKEY_EXPIRATION, arg); + return true; + case OPT_S2K_MSEC: { + int msec = 0; + if (!rnp::str_to_int(arg, msec) || !msec) { + ERR_MSG("Invalid s2k msec value: %s", arg); + return false; + } + cfg.set_int(CFG_S2K_MSEC, msec); + return true; + } + case OPT_PASSWDFD: + cfg.set_str(CFG_PASSFD, arg); + return true; + case OPT_PASSWD: + cfg.set_str(CFG_PASSWD, arg); + return true; + case OPT_RESULTS: + cfg.set_str(CFG_IO_RESS, arg); + return true; + case OPT_CIPHER: + return cli_rnp_set_cipher(cfg, arg); + case OPT_DEBUG: + ERR_MSG("Option --debug is deprecated, ignoring."); + return true; + case OPT_OUTPUT: + if (!arg) { + ERR_MSG("No output filename argument provided"); + return false; + } + cfg.set_str(CFG_OUTFILE, arg); + return true; + case OPT_OVERWRITE: + cfg.set_bool(CFG_OVERWRITE, true); + return true; + case OPT_FORCE: + cfg.set_bool(CFG_FORCE, true); + return true; + case OPT_SECRET: + cfg.set_bool(CFG_SECRET, true); + return true; + case OPT_WITH_SIGS: + cfg.set_bool(CFG_WITH_SIGS, true); + return true; + case OPT_REV_TYPE: { + std::string revtype = arg; + if (revtype == "0") { + revtype = "no"; + } else if (revtype == "1") { + revtype = "superseded"; + } else if (revtype == "2") { + revtype = "compromised"; + } else if (revtype == "3") { + revtype = "retired"; + } + cfg.set_str(CFG_REV_TYPE, revtype); + return true; + } + case OPT_REV_REASON: + cfg.set_str(CFG_REV_REASON, arg); + return true; + case OPT_PERMISSIVE: + cfg.set_bool(CFG_PERMISSIVE, true); + return true; + case OPT_NOTTY: + cfg.set_bool(CFG_NOTTY, true); + return true; + case OPT_FIX_25519_BITS: + cfg.set_bool(CFG_FIX_25519_BITS, true); + return true; + case OPT_CHK_25519_BITS: + cfg.set_bool(CFG_CHK_25519_BITS, true); + return true; + case OPT_CURTIME: + cfg.set_str(CFG_CURTIME, arg); + return true; + case OPT_ADD_SUBKEY: + cfg.set_bool(CFG_ADD_SUBKEY, true); + return true; + case OPT_SET_EXPIRE: + cfg.set_str(CFG_SET_KEY_EXPIRE, arg); + return true; + default: + *cmd = CMD_HELP; + return true; + } +} + +bool +rnpkeys_init(cli_rnp_t &rnp, const rnp_cfg &cfg) +{ + rnp_cfg rnpcfg; + rnpcfg.load_defaults(); + rnpcfg.set_int(CFG_NUMBITS, DEFAULT_RSA_NUMBITS); + rnpcfg.set_str(CFG_IO_RESS, ""); + rnpcfg.copy(cfg); + + if (!cli_cfg_set_keystore_info(rnpcfg)) { + ERR_MSG("fatal: cannot set keystore info"); + return false; + } + if (!rnp.init(rnpcfg)) { + ERR_MSG("fatal: failed to initialize rnpkeys"); + return false; + } + if (!cli_rnp_check_weak_hash(&rnp)) { + ERR_MSG("Weak hash algorithm detected. Pass --allow-weak-hash option if you really " + "want to use it."); + return false; + } + /* TODO: at some point we should check for error here */ + (void) rnp.load_keyrings(true); + return true; +} diff --git a/src/rnpkeys/rnpkeys.h b/src/rnpkeys/rnpkeys.h new file mode 100644 index 0000000..611fcc1 --- /dev/null +++ b/src/rnpkeys/rnpkeys.h @@ -0,0 +1,80 @@ +#ifndef RNPKEYS_H_ +#define RNPKEYS_H_ + +#include +#ifdef HAVE_SYS_PARAM_H +#include +#else +#include "uniwin.h" +#endif +#include "../rnp/fficli.h" +#include "logging.h" + +#define DEFAULT_RSA_NUMBITS 2048 + +typedef enum { + /* commands */ + CMD_NONE = 0, + CMD_LIST_KEYS = 260, + CMD_EXPORT_KEY, + CMD_IMPORT, + CMD_IMPORT_KEYS, + CMD_IMPORT_SIGS, + CMD_GENERATE_KEY, + CMD_EXPORT_REV, + CMD_REVOKE_KEY, + CMD_REMOVE_KEY, + CMD_EDIT_KEY, + CMD_VERSION, + CMD_HELP, + + /* options */ + OPT_KEY_STORE_FORMAT, + OPT_USERID, + OPT_HOMEDIR, + OPT_NUMBITS, + OPT_ALLOW_WEAK_HASH, + OPT_HASH_ALG, + OPT_COREDUMPS, + OPT_PASSWDFD, + OPT_PASSWD, + OPT_RESULTS, + OPT_CIPHER, + OPT_EXPERT, + OPT_OUTPUT, + OPT_OVERWRITE, + OPT_FORCE, + OPT_SECRET, + OPT_S2K_ITER, + OPT_S2K_MSEC, + OPT_EXPIRATION, + OPT_WITH_SIGS, + OPT_REV_TYPE, + OPT_REV_REASON, + OPT_PERMISSIVE, + OPT_NOTTY, + OPT_FIX_25519_BITS, + OPT_CHK_25519_BITS, + OPT_CURTIME, + OPT_ADD_SUBKEY, + OPT_SET_EXPIRE, + + /* debug */ + OPT_DEBUG +} optdefs_t; + +bool rnp_cmd(cli_rnp_t *rnp, optdefs_t cmd, const char *f); +bool setoption(rnp_cfg &cfg, optdefs_t *cmd, int val, const char *arg); +void print_usage(const char *usagemsg); + +/** + * @brief Initializes rnpkeys. Function allocates memory dynamically for + * rnp argument, which must be freed by the caller. + * + * @param rnp initialized rnp context + * @param cfg configuration with settings from command line + * @return true on success, or false otherwise. + */ +bool rnpkeys_init(cli_rnp_t &rnp, const rnp_cfg &cfg); + +#endif /* _rnpkeys_ */ diff --git a/src/rnpkeys/tui.cpp b/src/rnpkeys/tui.cpp new file mode 100644 index 0000000..73f26dc --- /dev/null +++ b/src/rnpkeys/tui.cpp @@ -0,0 +1,413 @@ +/* + * Copyright (c) 2017-2020, [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 +#ifdef _MSC_VER +#include "uniwin.h" +#else +#include +#endif +#include +#include +#include "rnp/rnpcfg.h" +#include "rnpkeys.h" +#include "defaults.h" +#include "file-utils.h" +#include "logging.h" + +/* ----------------------------------------------------------------------------- + * @brief Reads input from file pointer and converts it securelly to ints + * Partially based on ERR34-C from SEI CERT C Coding Standard + * + * @param fp pointer to opened pipe + * @param result[out] result read from file pointer and converted to int + * + * @returns true and value in result if integer was parsed correctly, + * otherwise false + * +-------------------------------------------------------------------------------- */ +static bool +rnp_secure_get_long_from_fd(FILE *fp, long &result, bool allow_empty = true) +{ + char buff[BUFSIZ]; + if (!fgets(buff, sizeof(buff), fp)) { + RNP_LOG("EOF or read error"); + return false; + } + + errno = 0; + char *end_ptr = NULL; + long num_long = strtol(buff, &end_ptr, 10); + if (ERANGE == errno) { + RNP_LOG("Number out of range"); + return false; + } + if (end_ptr == buff) { + return allow_empty; + } + if ('\n' != *end_ptr && '\0' != *end_ptr) { + RNP_LOG("Unexpected end of line"); + return false; + } + + result = num_long; + return true; +} + +static bool +is_rsa_keysize_supported(uint32_t keysize) +{ + return ((keysize >= 1024) && (keysize <= 4096) && !(keysize % 8)); +} + +static const char * +ask_curve_name(FILE *input_fp) +{ + std::vector curves; + static const char *const known_curves[] = { + "NIST P-256", + "NIST P-384", + "NIST P-521", + "brainpoolP256r1", + "brainpoolP384r1", + "brainpoolP512r1", + "secp256k1", + }; + const size_t curvenum = sizeof(known_curves) / sizeof(*known_curves); + + try { + std::copy_if(known_curves, + known_curves + curvenum, + std::back_inserter(curves), + [](const char *curve) { + bool supported = false; + return !rnp_supports_feature(RNP_FEATURE_CURVE, curve, &supported) && + supported; + }); + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + return NULL; + } + const size_t ccount = curves.size(); + if (!ccount) { + return NULL; + } + bool ok = false; + const char *result = NULL; + int attempts = 0; + do { + if (attempts >= 10) { + printf("Too many attempts. Aborting.\n"); + return NULL; + } + printf("Please select which elliptic curve you want:\n"); + for (size_t i = 0; i < ccount; i++) { + printf("\t(%zu) %s\n", i + 1, curves[i]); + } + printf("(default %s)> ", DEFAULT_CURVE); + long val = 0; + ok = rnp_secure_get_long_from_fd(input_fp, val) && (val > 0) && (val <= (long) ccount); + if (ok) { + result = curves[val - 1]; + } + attempts++; + } while (!ok); + + return result; +} + +static long +ask_rsa_bitlen(FILE *input_fp) +{ + long result = 0; + do { + result = DEFAULT_RSA_NUMBITS; + printf("Please provide bit length of the key (between 1024 and 4096):\n(default %d)> ", + DEFAULT_RSA_NUMBITS); + } while (!rnp_secure_get_long_from_fd(input_fp, result) || + !is_rsa_keysize_supported(result)); + return result; +} + +static long +ask_elgamal_bitlen(FILE *input_fp) +{ + do { + printf( + "Please provide bit length of the ElGamal key (between %d and %d):\n(default %d) > ", + ELGAMAL_MIN_P_BITLEN, + ELGAMAL_MAX_P_BITLEN, + DEFAULT_ELGAMAL_NUMBITS); + long result = DEFAULT_ELGAMAL_NUMBITS; + if (!rnp_secure_get_long_from_fd(input_fp, result)) { + continue; + } + if ((result >= ELGAMAL_MIN_P_BITLEN) && (result <= ELGAMAL_MAX_P_BITLEN)) { + // round up to multiple of 32 + result = ((result + 31) / 32) * 32; + printf("Bitlen of the key will be %lu\n", result); + return result; + } + } while (1); +} + +static long +ask_dsa_bitlen(FILE *input_fp) +{ + do { + printf( + "Please provide bit length of the DSA key (between %d and %d):\n(default %d) > ", + DSA_MIN_P_BITLEN, + DSA_MAX_P_BITLEN, + DSA_DEFAULT_P_BITLEN); + long result = DSA_DEFAULT_P_BITLEN; + if (!rnp_secure_get_long_from_fd(input_fp, result)) { + continue; + } + if ((result >= DSA_MIN_P_BITLEN) && (result <= DSA_MAX_P_BITLEN)) { + // round up to multiple of 64 + result = ((result + 63) / 64) * 64; + printf("Bitlen of the key will be %lu\n", result); + return result; + } + } while (1); +} + +static bool +rnpkeys_ask_generate_params(rnp_cfg &cfg, FILE *input_fp) +{ + long option = 0; + do { + printf("Please select what kind of key you want:\n" + "\t(1) RSA (Encrypt or Sign)\n" + "\t(16) DSA + ElGamal\n" + "\t(17) DSA + RSA\n" // TODO: See #584 + "\t(19) ECDSA + ECDH\n" + "\t(22) EDDSA + X25519\n" + "\t(99) SM2\n" + "> "); + if (!rnp_secure_get_long_from_fd(input_fp, option, false)) { + option = 0; + continue; + } + switch (option) { + case 1: { + int bits = ask_rsa_bitlen(input_fp); + cfg.set_str(CFG_KG_PRIMARY_ALG, RNP_ALGNAME_RSA); + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_RSA); + cfg.set_int(CFG_KG_PRIMARY_BITS, bits); + cfg.set_int(CFG_KG_SUBKEY_BITS, bits); + break; + } + case 16: { + int bits = ask_dsa_bitlen(input_fp); + cfg.set_str(CFG_KG_PRIMARY_ALG, RNP_ALGNAME_DSA); + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_ELGAMAL); + cfg.set_int(CFG_KG_PRIMARY_BITS, bits); + cfg.set_int(CFG_KG_SUBKEY_BITS, bits); + break; + } + case 17: { + int bits = ask_dsa_bitlen(input_fp); + cfg.set_str(CFG_KG_PRIMARY_ALG, RNP_ALGNAME_DSA); + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_RSA); + cfg.set_int(CFG_KG_PRIMARY_BITS, bits); + cfg.set_int(CFG_KG_SUBKEY_BITS, bits); + break; + } + case 19: { + const char *curve = ask_curve_name(input_fp); + if (!curve) { + return false; + } + cfg.set_str(CFG_KG_PRIMARY_ALG, RNP_ALGNAME_ECDSA); + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_ECDH); + cfg.set_str(CFG_KG_PRIMARY_CURVE, curve); + cfg.set_str(CFG_KG_SUBKEY_CURVE, curve); + break; + } + case 22: { + cfg.set_str(CFG_KG_PRIMARY_ALG, RNP_ALGNAME_EDDSA); + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_ECDH); + cfg.set_str(CFG_KG_SUBKEY_CURVE, "Curve25519"); + break; + } + case 99: { + cfg.set_str(CFG_KG_PRIMARY_ALG, RNP_ALGNAME_SM2); + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_SM2); + if (!cfg.has(CFG_KG_HASH)) { + cfg.set_str(CFG_KG_HASH, RNP_ALGNAME_SM3); + } + break; + } + default: + option = 0; + break; + } + } while (!option); + + return true; +} + +static bool +rnpkeys_ask_generate_params_subkey(rnp_cfg &cfg, FILE *input_fp) +{ + long option = 0; + do { + printf("Please select subkey algorithm you want:\n" + "\t(1) RSA\n" + "\t(16) ElGamal\n" + "\t(17) DSA\n" + "\t(18) ECDH\n" + "\t(19) ECDSA\n" + "\t(22) EDDSA\n" + "\t(99) SM2" + "> "); + if (!rnp_secure_get_long_from_fd(input_fp, option, false)) { + option = 0; + continue; + } + switch (option) { + case 1: { + int bits = ask_rsa_bitlen(input_fp); + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_RSA); + cfg.set_int(CFG_KG_SUBKEY_BITS, bits); + break; + } + case 16: { + int bits = ask_elgamal_bitlen(input_fp); + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_ELGAMAL); + cfg.set_int(CFG_KG_SUBKEY_BITS, bits); + break; + } + case 17: { + int bits = ask_dsa_bitlen(input_fp); + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_DSA); + cfg.set_int(CFG_KG_SUBKEY_BITS, bits); + break; + } + case 18: { + const char *curve = ask_curve_name(input_fp); + if (!curve) { + return false; + } + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_ECDH); + cfg.set_str(CFG_KG_SUBKEY_CURVE, curve); + break; + } + case 19: { + const char *curve = ask_curve_name(input_fp); + if (!curve) { + return false; + } + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_ECDSA); + cfg.set_str(CFG_KG_SUBKEY_CURVE, curve); + break; + } + case 22: { + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_EDDSA); + break; + } + case 99: { + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_SM2); + if (!cfg.has(CFG_KG_HASH)) { + cfg.set_str(CFG_KG_HASH, RNP_ALGNAME_SM3); + } + break; + } + default: + option = 0; + break; + } + } while (!option); + + return true; +} + +bool +cli_rnp_set_generate_params(rnp_cfg &cfg, bool subkey) +{ + bool res = true; + // hash algorithms for signing and protection + if (cfg.has(CFG_HASH)) { + cfg.set_str(CFG_KG_HASH, cfg.get_str(CFG_HASH)); + cfg.set_str(CFG_KG_PROT_HASH, cfg.get_str(CFG_HASH)); + } + + // key and subkey algorithms, bit length/curve + if (!cfg.get_bool(CFG_EXPERT)) { + cfg.set_str(CFG_KG_PRIMARY_ALG, RNP_ALGNAME_RSA); + cfg.set_int(CFG_KG_PRIMARY_BITS, cfg.get_int(CFG_NUMBITS)); + cfg.set_str(CFG_KG_SUBKEY_ALG, RNP_ALGNAME_RSA); + cfg.set_int(CFG_KG_SUBKEY_BITS, cfg.get_int(CFG_NUMBITS)); + } else { + FILE *input = stdin; + if (cfg.has(CFG_USERINPUTFD)) { + int inputfd = dup(cfg.get_int(CFG_USERINPUTFD)); + if (inputfd != -1) { + input = rnp_fdopen(inputfd, "r"); + if (!input) { + close(inputfd); + } + } + } + if (!input) { + return false; + } + if (subkey) { + res = rnpkeys_ask_generate_params_subkey(cfg, input); + } else { + res = rnpkeys_ask_generate_params(cfg, input); + } + if (input != stdin) { + fclose(input); + } + if (!res) { + return false; + } + } + + // make sure hash algorithms are set + if (!cfg.has(CFG_KG_HASH)) { + cfg.set_str(CFG_KG_HASH, DEFAULT_HASH_ALG); + } + if (!cfg.has(CFG_KG_PROT_HASH)) { + cfg.set_str(CFG_KG_PROT_HASH, DEFAULT_HASH_ALG); + } + + // protection symmetric algorithm + cfg.set_str(CFG_KG_PROT_ALG, + cfg.has(CFG_CIPHER) ? cfg.get_str(CFG_CIPHER) : DEFAULT_SYMM_ALG); + // protection iterations count + size_t iterations = cfg.get_int(CFG_S2K_ITER); + if (!iterations) { + res = res && !rnp_calculate_iterations(cfg.get_str(CFG_KG_PROT_HASH).c_str(), + cfg.get_int(CFG_S2K_MSEC), + &iterations); + } + cfg.set_int(CFG_KG_PROT_ITERATIONS, iterations); + return res; +} -- cgit v1.2.3