diff options
Diffstat (limited to 'src/librekey')
-rw-r--r-- | src/librekey/g23_sexp.hpp | 71 | ||||
-rw-r--r-- | src/librekey/kbx_blob.hpp | 162 | ||||
-rw-r--r-- | src/librekey/key_store_g10.cpp | 1243 | ||||
-rw-r--r-- | src/librekey/key_store_g10.h | 42 | ||||
-rw-r--r-- | src/librekey/key_store_kbx.cpp | 706 | ||||
-rw-r--r-- | src/librekey/key_store_kbx.h | 36 | ||||
-rw-r--r-- | src/librekey/key_store_pgp.cpp | 241 | ||||
-rw-r--r-- | src/librekey/key_store_pgp.h | 83 | ||||
-rw-r--r-- | src/librekey/rnp_key_store.cpp | 803 |
9 files changed, 3387 insertions, 0 deletions
diff --git a/src/librekey/g23_sexp.hpp b/src/librekey/g23_sexp.hpp new file mode 100644 index 0000000..b888680 --- /dev/null +++ b/src/librekey/g23_sexp.hpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 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 RIBOSE, INC. AND CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * 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. + */ + +#ifndef RNP_G23_SEXP_HPP +#define RNP_G23_SEXP_HPP + +#include "sexp/sexp.h" +#include "sexp/ext-key-format.h" + +#define SXP_MAX_DEPTH 30 + +class gnupg_sexp_t; +typedef std::shared_ptr<gnupg_sexp_t> p_gnupg_sexp; + +class gnupg_sexp_t : public sexp::sexp_list_t { + /* write gnupg_sexp_t contents, adding padding, for the further encryption */ + rnp::secure_vector<uint8_t> write_padded(size_t padblock) const; + + public: + void + add(const std::string &str) + { + push_back(std::shared_ptr<sexp::sexp_string_t>(new sexp::sexp_string_t(str))); + }; + void + add(const uint8_t *data, size_t size) + { + push_back(std::shared_ptr<sexp::sexp_string_t>(new sexp::sexp_string_t(data, size))); + }; + void add(unsigned u); + p_gnupg_sexp add_sub(); + void add_mpi(const std::string &name, const pgp_mpi_t &val); + void add_curve(const std::string &name, const pgp_ec_key_t &key); + void add_pubkey(const pgp_key_pkt_t &key); + void add_seckey(const pgp_key_pkt_t &key); + void add_protected_seckey(pgp_key_pkt_t & seckey, + const std::string & password, + rnp::SecurityContext &ctx); + bool parse(const char *r_bytes, size_t r_length, size_t depth = 1); + bool write(pgp_dest_t &dst) const noexcept; +}; + +class gnupg_extended_private_key_t : public ext_key_format::extended_private_key_t { + public: + bool parse(const char *r_bytes, size_t r_length, size_t depth = 1); +}; + +#endif diff --git a/src/librekey/kbx_blob.hpp b/src/librekey/kbx_blob.hpp new file mode 100644 index 0000000..274413c --- /dev/null +++ b/src/librekey/kbx_blob.hpp @@ -0,0 +1,162 @@ +/* + * Copyright (c) 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 RIBOSE, INC. AND CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * 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. + */ + +#ifndef RNP_KBX_BLOB_HPP +#define RNP_KBX_BLOB_HPP + +typedef enum : uint8_t { + KBX_EMPTY_BLOB = 0, + KBX_HEADER_BLOB = 1, + KBX_PGP_BLOB = 2, + KBX_X509_BLOB = 3 +} kbx_blob_type_t; + +class kbx_blob_t { + protected: + kbx_blob_type_t type_; + std::vector<uint8_t> image_; + + uint8_t ru8(size_t idx); + uint16_t ru16(size_t idx); + uint32_t ru32(size_t idx); + + public: + virtual ~kbx_blob_t() = default; + kbx_blob_t(std::vector<uint8_t> &data); + virtual bool + parse() + { + return true; + }; + + kbx_blob_type_t + type() + { + return type_; + } + + std::vector<uint8_t> & + image() + { + return image_; + } + + uint32_t + length() const noexcept + { + return image_.size(); + } +}; + +class kbx_header_blob_t : public kbx_blob_t { + protected: + uint8_t version_{}; + uint16_t flags_{}; + uint32_t file_created_at_{}; + uint32_t last_maintenance_run_{}; + + public: + kbx_header_blob_t(std::vector<uint8_t> &data) : kbx_blob_t(data){}; + bool parse(); + + uint32_t + file_created_at() + { + return file_created_at_; + } +}; + +typedef struct { + uint8_t fp[PGP_FINGERPRINT_SIZE]; + uint32_t keyid_offset; + uint16_t flags; +} kbx_pgp_key_t; + +typedef struct { + uint32_t offset; + uint32_t length; + uint16_t flags; + uint8_t validity; +} kbx_pgp_uid_t; + +typedef struct { + uint32_t expired; +} kbx_pgp_sig_t; + +class kbx_pgp_blob_t : public kbx_blob_t { + protected: + uint8_t version_{}; + uint16_t flags_{}; + uint32_t keyblock_offset_{}; + uint32_t keyblock_length_{}; + + std::vector<uint8_t> sn_{}; + std::vector<kbx_pgp_key_t> keys_{}; + std::vector<kbx_pgp_uid_t> uids_{}; + std::vector<kbx_pgp_sig_t> sigs_{}; + + uint8_t ownertrust_{}; + uint8_t all_validity_{}; + + uint32_t recheck_after_{}; + uint32_t latest_timestamp_{}; + uint32_t blob_created_at_{}; + + public: + kbx_pgp_blob_t(std::vector<uint8_t> &data) : kbx_blob_t(data){}; + + uint32_t + keyblock_offset() + { + return keyblock_offset_; + } + + uint32_t + keyblock_length() + { + return keyblock_length_; + } + + size_t + nkeys() + { + return keys_.size(); + } + size_t + nuids() + { + return uids_.size(); + } + size_t + nsigs() + { + return sigs_.size(); + } + + bool parse(); +}; + +#endif diff --git a/src/librekey/key_store_g10.cpp b/src/librekey/key_store_g10.cpp new file mode 100644 index 0000000..dcf3fe1 --- /dev/null +++ b/src/librekey/key_store_g10.cpp @@ -0,0 +1,1243 @@ +/* + * Copyright (c) 2017-2022, [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 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. + */ + +#include <memory> +#include <sstream> +#include <string.h> +#include <stdlib.h> +#include <limits.h> +#include <time.h> +#include "config.h" + +#include <librepgp/stream-packet.h> +#include "key_store_pgp.h" +#include "key_store_g10.h" + +#include "crypto/common.h" +#include "crypto/mem.h" +#include "crypto/cipher.hpp" +#include "pgp-key.h" +#include "time-utils.h" + +#include "g23_sexp.hpp" +using namespace ext_key_format; +using namespace sexp; + +#define G10_CBC_IV_SIZE 16 + +#define G10_OCB_NONCE_SIZE 12 + +#define G10_SHA1_HASH_SIZE 20 + +#define G10_PROTECTED_AT_SIZE 15 + +typedef struct format_info { + pgp_symm_alg_t cipher; + pgp_cipher_mode_t cipher_mode; + pgp_hash_alg_t hash_alg; + size_t cipher_block_size; + const char * g10_type; + size_t iv_size; + size_t tag_length; + bool with_associated_data; + bool disable_padding; +} format_info; + +static bool g10_calculated_hash(const pgp_key_pkt_t &key, + const char * protected_at, + uint8_t * checksum); + +static const format_info formats[] = {{PGP_SA_AES_128, + PGP_CIPHER_MODE_CBC, + PGP_HASH_SHA1, + 16, + "openpgp-s2k3-sha1-aes-cbc", + G10_CBC_IV_SIZE, + 0, + false, + true}, + {PGP_SA_AES_256, + PGP_CIPHER_MODE_CBC, + PGP_HASH_SHA1, + 16, + "openpgp-s2k3-sha1-aes256-cbc", + G10_CBC_IV_SIZE, + 0, + false, + true}, + {PGP_SA_AES_128, + PGP_CIPHER_MODE_OCB, + PGP_HASH_SHA1, + 16, + "openpgp-s2k3-ocb-aes", + G10_OCB_NONCE_SIZE, + 16, + true, + true}}; + +static const id_str_pair g10_alg_aliases[] = { + {PGP_PKA_RSA, "rsa"}, + {PGP_PKA_RSA, "openpgp-rsa"}, + {PGP_PKA_RSA, "oid.1.2.840.113549.1.1.1"}, + {PGP_PKA_RSA, "oid.1.2.840.113549.1.1.1"}, + {PGP_PKA_ELGAMAL, "elg"}, + {PGP_PKA_ELGAMAL, "elgamal"}, + {PGP_PKA_ELGAMAL, "openpgp-elg"}, + {PGP_PKA_ELGAMAL, "openpgp-elg-sig"}, + {PGP_PKA_DSA, "dsa"}, + {PGP_PKA_DSA, "openpgp-dsa"}, + {PGP_PKA_ECDSA, "ecc"}, + {PGP_PKA_ECDSA, "ecdsa"}, + {PGP_PKA_ECDH, "ecdh"}, + {PGP_PKA_EDDSA, "eddsa"}, + {0, NULL}, +}; + +static const id_str_pair g10_curve_aliases[] = { + {PGP_CURVE_NIST_P_256, "NIST P-256"}, + {PGP_CURVE_NIST_P_256, "1.2.840.10045.3.1.7"}, + {PGP_CURVE_NIST_P_256, "prime256v1"}, + {PGP_CURVE_NIST_P_256, "secp256r1"}, + {PGP_CURVE_NIST_P_256, "nistp256"}, + {PGP_CURVE_NIST_P_384, "NIST P-384"}, + {PGP_CURVE_NIST_P_384, "secp384r1"}, + {PGP_CURVE_NIST_P_384, "1.3.132.0.34"}, + {PGP_CURVE_NIST_P_384, "nistp384"}, + {PGP_CURVE_NIST_P_521, "NIST P-521"}, + {PGP_CURVE_NIST_P_521, "secp521r1"}, + {PGP_CURVE_NIST_P_521, "1.3.132.0.35"}, + {PGP_CURVE_NIST_P_521, "nistp521"}, + {PGP_CURVE_25519, "Curve25519"}, + {PGP_CURVE_25519, "1.3.6.1.4.1.3029.1.5.1"}, + {PGP_CURVE_ED25519, "Ed25519"}, + {PGP_CURVE_ED25519, "1.3.6.1.4.1.11591.15.1"}, + {PGP_CURVE_BP256, "brainpoolP256r1"}, + {PGP_CURVE_BP256, "1.3.36.3.3.2.8.1.1.7"}, + {PGP_CURVE_BP384, "brainpoolP384r1"}, + {PGP_CURVE_BP384, "1.3.36.3.3.2.8.1.1.11"}, + {PGP_CURVE_BP512, "brainpoolP512r1"}, + {PGP_CURVE_BP512, "1.3.36.3.3.2.8.1.1.13"}, + {PGP_CURVE_P256K1, "secp256k1"}, + {PGP_CURVE_P256K1, "1.3.132.0.10"}, + {0, NULL}, +}; + +static const id_str_pair g10_curve_names[] = { + {PGP_CURVE_NIST_P_256, "NIST P-256"}, + {PGP_CURVE_NIST_P_384, "NIST P-384"}, + {PGP_CURVE_NIST_P_521, "NIST P-521"}, + {PGP_CURVE_ED25519, "Ed25519"}, + {PGP_CURVE_25519, "Curve25519"}, + {PGP_CURVE_BP256, "brainpoolP256r1"}, + {PGP_CURVE_BP384, "brainpoolP384r1"}, + {PGP_CURVE_BP512, "brainpoolP512r1"}, + {PGP_CURVE_P256K1, "secp256k1"}, + {0, NULL}, +}; + +static const format_info * +find_format(pgp_symm_alg_t cipher, pgp_cipher_mode_t mode, pgp_hash_alg_t hash_alg) +{ + for (size_t i = 0; i < ARRAY_SIZE(formats); i++) { + if (formats[i].cipher == cipher && formats[i].cipher_mode == mode && + formats[i].hash_alg == hash_alg) { + return &formats[i]; + } + } + return NULL; +} + +static const format_info * +parse_format(const char *format, size_t format_len) +{ + for (size_t i = 0; i < ARRAY_SIZE(formats); i++) { + if (strlen(formats[i].g10_type) == format_len && + !strncmp(formats[i].g10_type, format, format_len)) { + return &formats[i]; + } + } + return NULL; +} + +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) + +void +gnupg_sexp_t::add(unsigned u) +{ + char s[sizeof(STR(UINT_MAX)) + 1]; + snprintf(s, sizeof(s), "%u", u); + push_back(std::make_shared<sexp_string_t>(s)); +} + +std::shared_ptr<gnupg_sexp_t> +gnupg_sexp_t::add_sub() +{ + auto res = std::make_shared<gnupg_sexp_t>(); + push_back(res); + return res; +} + +/* + * Parse S-expression + * https://people.csail.mit.edu/rivest/Sexp.txt + * sexp library supports canonical and advanced transport formats + * as well as base64 encoding of canonical + */ + +bool +gnupg_sexp_t::parse(const char *r_bytes, size_t r_length, size_t depth) +{ + bool res = false; + std::istringstream iss(std::string(r_bytes, r_length)); + try { + sexp_input_stream_t sis(&iss, depth); + sexp_list_t::parse(sis.set_byte_size(8)->get_char()); + res = true; + } catch (sexp_exception_t &e) { + RNP_LOG("%s", e.what()); + } + return res; +} + +/* + * Parse gnupg extended private key file ("G23") + * https://github.com/gpg/gnupg/blob/main/agent/keyformat.txt + */ + +bool +gnupg_extended_private_key_t::parse(const char *r_bytes, size_t r_length, size_t depth) +{ + bool res = false; + std::istringstream iss(std::string(r_bytes, r_length)); + try { + ext_key_input_stream_t g23_is(&iss, depth); + g23_is.scan(*this); + res = true; + } catch (sexp_exception_t &e) { + RNP_LOG("%s", e.what()); + } + return res; +} + +static const sexp_list_t * +lookup_var(const sexp_list_t *list, const std::string &name) noexcept +{ + const sexp_list_t *res = nullptr; + // We are looking for a list element (condition 1) + // that: + // -- has at least two SEXP elements (condition 2) + // -- has a SEXP string at 0 postion (condition 3) + // matching given name (condition 4) + auto match = [name](const std::shared_ptr<sexp_object_t> &ptr) { + bool r = false; + auto r1 = ptr->sexp_list_view(); + if (r1 && r1->size() >= 2) { // conditions (1) and (2) + auto r2 = r1->sexp_string_at(0); + if (r2 && r2 == name) // conditions (3) and (4) + r = true; + } + return r; + }; + auto r3 = std::find_if(list->begin(), list->end(), match); + if (r3 == list->end()) + RNP_LOG("Haven't got variable '%s'", name.c_str()); + else + res = (*r3)->sexp_list_view(); + return res; +} + +static const sexp_string_t * +lookup_var_data(const sexp_list_t *list, const std::string &name) noexcept +{ + const sexp_list_t *var = lookup_var(list, name); + if (!var) { + return NULL; + } + + if (!var->at(1)->is_sexp_string()) { + RNP_LOG("Expected block value"); + return NULL; + } + + return var->sexp_string_at(1); +} + +static bool +read_mpi(const sexp_list_t *list, const std::string &name, pgp_mpi_t &val) noexcept +{ + const sexp_string_t *data = lookup_var_data(list, name); + if (!data) { + return false; + } + + /* strip leading zero */ + const auto &bytes = data->get_string(); + if ((bytes.size() > 1) && !bytes[0] && (bytes[1] & 0x80)) { + return mem2mpi(&val, bytes.data() + 1, bytes.size() - 1); + } + return mem2mpi(&val, bytes.data(), bytes.size()); +} + +static bool +read_curve(const sexp_list_t *list, const std::string &name, pgp_ec_key_t &key) noexcept +{ + const sexp_string_t *data = lookup_var_data(list, name); + if (!data) { + return false; + } + + const auto &bytes = data->get_string(); + pgp_curve_t curve = static_cast<pgp_curve_t>( + id_str_pair::lookup(g10_curve_aliases, data->get_string(), PGP_CURVE_UNKNOWN)); + if (curve != PGP_CURVE_UNKNOWN) { + key.curve = curve; + return true; + } + RNP_LOG("Unknown curve: %.*s", (int) bytes.size(), (char *) bytes.data()); + return false; +} + +void +gnupg_sexp_t::add_mpi(const std::string &name, const pgp_mpi_t &mpi) +{ + auto sub_s_exp = add_sub(); + sub_s_exp->push_back(std::make_shared<sexp_string_t>(name)); + auto value_block = std::make_shared<sexp_string_t>(); + sub_s_exp->push_back(value_block); + + sexp_simple_string_t data; + size_t len = mpi_bytes(&mpi); + size_t idx; + + for (idx = 0; (idx < len) && !mpi.mpi[idx]; idx++) + ; + + if (idx < len) { + if (mpi.mpi[idx] & 0x80) { + data.append(0); + data.std::basic_string<uint8_t>::append(mpi.mpi + idx, len - idx); + } else { + data.assign(mpi.mpi + idx, mpi.mpi + len); + } + value_block->set_string(data); + } +} + +void +gnupg_sexp_t::add_curve(const std::string &name, const pgp_ec_key_t &key) +{ + const char *curve = id_str_pair::lookup(g10_curve_names, key.curve, NULL); + if (!curve) { + RNP_LOG("unknown curve"); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + + auto psub_s_exp = add_sub(); + psub_s_exp->add(name); + psub_s_exp->add(curve); + + if ((key.curve != PGP_CURVE_ED25519) && (key.curve != PGP_CURVE_25519)) { + return; + } + + psub_s_exp = add_sub(); + psub_s_exp->add("flags"); + psub_s_exp->add((key.curve == PGP_CURVE_ED25519) ? "eddsa" : "djb-tweak"); +} + +static bool +parse_pubkey(pgp_key_pkt_t &pubkey, const sexp_list_t *s_exp, pgp_pubkey_alg_t alg) +{ + pubkey.version = PGP_V4; + pubkey.alg = alg; + pubkey.material.alg = alg; + switch (alg) { + case PGP_PKA_DSA: + if (!read_mpi(s_exp, "p", pubkey.material.dsa.p) || + !read_mpi(s_exp, "q", pubkey.material.dsa.q) || + !read_mpi(s_exp, "g", pubkey.material.dsa.g) || + !read_mpi(s_exp, "y", pubkey.material.dsa.y)) { + return false; + } + break; + + case PGP_PKA_RSA: + case PGP_PKA_RSA_ENCRYPT_ONLY: + case PGP_PKA_RSA_SIGN_ONLY: + if (!read_mpi(s_exp, "n", pubkey.material.rsa.n) || + !read_mpi(s_exp, "e", pubkey.material.rsa.e)) { + return false; + } + break; + + case PGP_PKA_ELGAMAL: + case PGP_PKA_ELGAMAL_ENCRYPT_OR_SIGN: + if (!read_mpi(s_exp, "p", pubkey.material.eg.p) || + !read_mpi(s_exp, "g", pubkey.material.eg.g) || + !read_mpi(s_exp, "y", pubkey.material.eg.y)) { + return false; + } + break; + case PGP_PKA_ECDSA: + case PGP_PKA_ECDH: + case PGP_PKA_EDDSA: + if (!read_curve(s_exp, "curve", pubkey.material.ec) || + !read_mpi(s_exp, "q", pubkey.material.ec.p)) { + return false; + } + if (pubkey.material.ec.curve == PGP_CURVE_ED25519) { + /* need to adjust it here since 'ecc' key type defaults to ECDSA */ + pubkey.alg = PGP_PKA_EDDSA; + pubkey.material.alg = PGP_PKA_EDDSA; + } + break; + default: + RNP_LOG("Unsupported public key algorithm: %d", (int) alg); + return false; + } + + return true; +} + +static bool +parse_seckey(pgp_key_pkt_t &seckey, const sexp_list_t *s_exp, pgp_pubkey_alg_t alg) +{ + switch (alg) { + case PGP_PKA_DSA: + if (!read_mpi(s_exp, "x", seckey.material.dsa.x)) { + return false; + } + break; + case PGP_PKA_RSA: + case PGP_PKA_RSA_ENCRYPT_ONLY: + case PGP_PKA_RSA_SIGN_ONLY: + if (!read_mpi(s_exp, "d", seckey.material.rsa.d) || + !read_mpi(s_exp, "p", seckey.material.rsa.p) || + !read_mpi(s_exp, "q", seckey.material.rsa.q) || + !read_mpi(s_exp, "u", seckey.material.rsa.u)) { + return false; + } + break; + case PGP_PKA_ELGAMAL: + case PGP_PKA_ELGAMAL_ENCRYPT_OR_SIGN: + if (!read_mpi(s_exp, "x", seckey.material.eg.x)) { + return false; + } + break; + case PGP_PKA_ECDSA: + case PGP_PKA_ECDH: + case PGP_PKA_EDDSA: + if (!read_mpi(s_exp, "d", seckey.material.ec.x)) { + return false; + } + break; + default: + RNP_LOG("Unsupported public key algorithm: %d", (int) alg); + return false; + } + + seckey.material.secret = true; + return true; +} + +static bool +decrypt_protected_section(const sexp_simple_string_t &encrypted_data, + const pgp_key_pkt_t & seckey, + const std::string & password, + gnupg_sexp_t & r_s_exp, + uint8_t * associated_data, + size_t associated_data_len) +{ + const format_info * info = NULL; + unsigned keysize = 0; + uint8_t derived_key[PGP_MAX_KEY_SIZE]; + uint8_t * decrypted_data = NULL; + size_t decrypted_data_len = 0; + size_t output_written = 0; + size_t input_consumed = 0; + std::unique_ptr<Cipher> dec; + bool ret = false; + + const char *decrypted_bytes; + size_t s_exp_len; + + // sanity checks + const pgp_key_protection_t &prot = seckey.sec_protection; + keysize = pgp_key_size(prot.symm_alg); + if (!keysize) { + RNP_LOG("parse_seckey: unknown symmetric algo"); + goto done; + } + // find the protection format in our table + info = find_format(prot.symm_alg, prot.cipher_mode, prot.s2k.hash_alg); + if (!info) { + RNP_LOG("Unsupported format, alg: %d, chiper_mode: %d, hash: %d", + prot.symm_alg, + prot.cipher_mode, + prot.s2k.hash_alg); + goto done; + } + + // derive the key + if (pgp_s2k_iterated(prot.s2k.hash_alg, + derived_key, + keysize, + password.c_str(), + prot.s2k.salt, + prot.s2k.iterations)) { + RNP_LOG("pgp_s2k_iterated failed"); + goto done; + } + + // decrypt + decrypted_data = (uint8_t *) malloc(encrypted_data.size()); + if (decrypted_data == NULL) { + RNP_LOG("can't allocate memory"); + goto done; + } + dec = Cipher::decryption( + info->cipher, info->cipher_mode, info->tag_length, info->disable_padding); + if (!dec || !dec->set_key(derived_key, keysize)) { + goto done; + } + if (associated_data != nullptr && associated_data_len != 0) { + if (!dec->set_ad(associated_data, associated_data_len)) { + goto done; + } + } + // Nonce shall be the last chunk of associated data + if (!dec->set_iv(prot.iv, info->iv_size)) { + goto done; + } + if (!dec->finish(decrypted_data, + encrypted_data.size(), + &output_written, + encrypted_data.data(), + encrypted_data.size(), + &input_consumed)) { + goto done; + } + decrypted_data_len = output_written; + s_exp_len = decrypted_data_len; + decrypted_bytes = (const char *) decrypted_data; + + // parse and validate the decrypted s-exp + + if (!r_s_exp.parse(decrypted_bytes, s_exp_len, SXP_MAX_DEPTH)) { + goto done; + } + if (!r_s_exp.size() || r_s_exp.at(0)->is_sexp_string()) { + RNP_LOG("Hasn't got sub s-exp with key data."); + goto done; + } + ret = true; +done: + if (!ret) { + r_s_exp.clear(); + } + secure_clear(decrypted_data, decrypted_data_len); + free(decrypted_data); + return ret; +} + +static bool +parse_protected_seckey(pgp_key_pkt_t &seckey, const sexp_list_t *list, const char *password) +{ + // find and validate the protected section + const sexp_list_t *protected_key = lookup_var(list, "protected"); + if (!protected_key) { + RNP_LOG("missing protected section"); + return false; + } + if (protected_key->size() != 4 || !protected_key->at(1)->is_sexp_string() || + protected_key->at(2)->is_sexp_string() || !protected_key->at(3)->is_sexp_string()) { + RNP_LOG("Wrong protected format, expected: (protected mode (params) " + "encrypted_octet_string)\n"); + return false; + } + + // lookup the protection format + auto & fmt_bt = protected_key->sexp_string_at(1)->get_string(); + const format_info *format = parse_format((const char *) fmt_bt.data(), fmt_bt.size()); + if (!format) { + RNP_LOG("Unsupported protected mode: '%.*s'\n", + (int) fmt_bt.size(), + (const char *) fmt_bt.data()); + return false; + } + + // fill in some fields based on the lookup above + pgp_key_protection_t &prot = seckey.sec_protection; + prot.symm_alg = format->cipher; + prot.cipher_mode = format->cipher_mode; + prot.s2k.hash_alg = format->hash_alg; + + // locate and validate the protection parameters + auto params = protected_key->sexp_list_at(2); + if (params->size() != 2 || params->at(0)->is_sexp_string() || + !params->at(1)->is_sexp_string()) { + RNP_LOG("Wrong params format, expected: ((hash salt no_of_iterations) iv)\n"); + return false; + } + + // locate and validate the (hash salt no_of_iterations) exp + auto alg = params->sexp_list_at(0); + if (alg->size() != 3 || !alg->at(0)->is_sexp_string() || !alg->at(1)->is_sexp_string() || + !alg->at(2)->is_sexp_string()) { + RNP_LOG("Wrong params sub-level format, expected: (hash salt no_of_iterations)\n"); + return false; + } + auto &hash_bt = alg->sexp_string_at(0)->get_string(); + if (hash_bt != "sha1") { + RNP_LOG("Wrong hashing algorithm, should be sha1 but %.*s\n", + (int) hash_bt.size(), + (const char *) hash_bt.data()); + return false; + } + + // fill in some constant values + prot.s2k.hash_alg = PGP_HASH_SHA1; + prot.s2k.usage = PGP_S2KU_ENCRYPTED_AND_HASHED; + prot.s2k.specifier = PGP_S2KS_ITERATED_AND_SALTED; + + // check salt size + auto &salt_bt = alg->sexp_string_at(1)->get_string(); + if (salt_bt.size() != PGP_SALT_SIZE) { + RNP_LOG("Wrong salt size, should be %d but %d\n", PGP_SALT_SIZE, (int) salt_bt.size()); + return false; + } + + // salt + memcpy(prot.s2k.salt, salt_bt.data(), salt_bt.size()); + // s2k iterations + auto iter = alg->sexp_string_at(2); + prot.s2k.iterations = iter->as_unsigned(); + if (prot.s2k.iterations == UINT_MAX) { + RNP_LOG("Wrong numbers of iteration, %.*s\n", + (int) iter->get_string().size(), + (const char *) iter->get_string().data()); + return false; + } + + // iv + auto &iv_bt = params->sexp_string_at(1)->get_string(); + if (iv_bt.size() != format->iv_size) { + RNP_LOG("Wrong nonce size, should be %zu but %zu\n", format->iv_size, iv_bt.size()); + return false; + } + memcpy(prot.iv, iv_bt.data(), iv_bt.size()); + + // we're all done if no password was provided (decryption not requested) + if (!password) { + seckey.material.secret = false; + return true; + } + + // password was provided, so decrypt + auto & enc_bt = protected_key->sexp_string_at(3)->get_string(); + gnupg_sexp_t decrypted_s_exp; + + // Build associated data (AD) that is not included in the ciphertext but that should be + // authenticated. gnupg builds AD as follows (file 'protect.c' do_encryption/do_decryption + // functions) + // -- "protected-private-key" section content + // -- less "protected" subsection + // -- serialized in canonical format + std::string associated_data; + if (format->with_associated_data) { + std::ostringstream oss(std::ios_base::binary); + sexp_output_stream_t os(&oss); + os.var_put_char('('); + for_each(list->begin(), list->end(), [&](const std::shared_ptr<sexp_object_t> &obj) { + if (obj->sexp_list_view() != protected_key) + obj->print_canonical(&os); + }); + os.var_put_char(')'); + associated_data = oss.str(); + } + + if (!decrypt_protected_section( + enc_bt, + seckey, + password, + decrypted_s_exp, + format->with_associated_data ? (uint8_t *) associated_data.data() : nullptr, + format->with_associated_data ? associated_data.length() : 0)) { + return false; + } + // see if we have a protected-at section + char protected_at[G10_PROTECTED_AT_SIZE] = {0}; + auto protected_at_data = lookup_var_data(list, "protected-at"); + if (protected_at_data) { + if (protected_at_data->get_string().size() != G10_PROTECTED_AT_SIZE) { + RNP_LOG("protected-at has wrong length: %zu, expected, %d\n", + protected_at_data->get_string().size(), + G10_PROTECTED_AT_SIZE); + return false; + } + memcpy(protected_at, + protected_at_data->get_string().data(), + protected_at_data->get_string().size()); + } + // parse MPIs + if (!parse_seckey(seckey, decrypted_s_exp.sexp_list_at(0), seckey.alg)) { + RNP_LOG("failed to parse seckey"); + return false; + } + // check hash, if present + if (decrypted_s_exp.size() > 1) { + if (decrypted_s_exp.at(1)->is_sexp_string()) { + RNP_LOG("Wrong hash block type."); + return false; + } + auto sub_el = decrypted_s_exp.sexp_list_at(1); + if (sub_el->size() < 3 || !sub_el->at(0)->is_sexp_string() || + !sub_el->at(1)->is_sexp_string() || !sub_el->at(2)->is_sexp_string()) { + RNP_LOG("Wrong hash block structure."); + return false; + } + + auto &hkey = sub_el->sexp_string_at(0)->get_string(); + if (hkey != "hash") { + RNP_LOG("Has got wrong hash block at encrypted key data."); + return false; + } + auto &halg = sub_el->sexp_string_at(1)->get_string(); + if (halg != "sha1") { + RNP_LOG("Supported only sha1 hash at encrypted private key."); + return false; + } + uint8_t checkhash[G10_SHA1_HASH_SIZE]; + if (!g10_calculated_hash(seckey, protected_at, checkhash)) { + RNP_LOG("failed to calculate hash"); + return false; + } + auto &hval = sub_el->sexp_string_at(2)->get_string(); + if (hval.size() != G10_SHA1_HASH_SIZE || + memcmp(checkhash, hval.data(), G10_SHA1_HASH_SIZE)) { + RNP_LOG("Incorrect hash at encrypted private key."); + return false; + } + } + seckey.material.secret = true; + return true; +} + +static bool +g23_parse_seckey(pgp_key_pkt_t &seckey, + const uint8_t *data, + size_t data_len, + const char * password) +{ + gnupg_extended_private_key_t g23_extended_key; + + const char *bytes = (const char *) data; + if (!g23_extended_key.parse(bytes, data_len, SXP_MAX_DEPTH)) { + RNP_LOG("Failed to parse s-exp."); + return false; + } + // Although the library parses full g23 extended key + // we extract and use g10 part only + const sexp_list_t &g10_key = g23_extended_key.key; + + /* expected format: + * (<type> + * (<algo> + * (x <mpi>) + * (y <mpi>) + * ) + * ) + */ + + if (g10_key.size() != 2 || !g10_key.at(0)->is_sexp_string() || + !g10_key.at(1)->is_sexp_list()) { + RNP_LOG("Wrong format, expected: (<type> (...))"); + return false; + } + + bool is_protected = false; + + auto &name = g10_key.sexp_string_at(0)->get_string(); + if (name == "private-key") { + is_protected = false; + } else if (name == "protected-private-key") { + is_protected = true; + } else { + RNP_LOG("Unsupported top-level block: '%.*s'", + (int) name.size(), + (const char *) name.data()); + return false; + } + + auto alg_s_exp = g10_key.sexp_list_at(1); + if (alg_s_exp->size() < 2) { + RNP_LOG("Wrong count of algorithm-level elements: %zu", alg_s_exp->size()); + return false; + } + + if (!alg_s_exp->at(0)->is_sexp_string()) { + RNP_LOG("Expected block with algorithm name, but has s-exp"); + return false; + } + + auto & alg_bt = alg_s_exp->sexp_string_at(0)->get_string(); + pgp_pubkey_alg_t alg = static_cast<pgp_pubkey_alg_t>( + id_str_pair::lookup(g10_alg_aliases, alg_bt.c_str(), PGP_PKA_NOTHING)); + if (alg == PGP_PKA_NOTHING) { + RNP_LOG( + "Unsupported algorithm: '%.*s'", (int) alg_bt.size(), (const char *) alg_bt.data()); + return false; + } + + bool ret = false; + if (!parse_pubkey(seckey, alg_s_exp, alg)) { + RNP_LOG("failed to parse pubkey"); + goto done; + } + + if (is_protected) { + if (!parse_protected_seckey(seckey, alg_s_exp, password)) { + goto done; + } + } else { + seckey.sec_protection.s2k.usage = PGP_S2KU_NONE; + seckey.sec_protection.symm_alg = PGP_SA_PLAINTEXT; + seckey.sec_protection.s2k.hash_alg = PGP_HASH_UNKNOWN; + if (!parse_seckey(seckey, alg_s_exp, alg)) { + RNP_LOG("failed to parse seckey"); + goto done; + } + } + ret = true; +done: + if (!ret) { + seckey = pgp_key_pkt_t(); + } + return ret; +} + +pgp_key_pkt_t * +g10_decrypt_seckey(const pgp_rawpacket_t &raw, + const pgp_key_pkt_t & pubkey, + const char * password) +{ + if (!password) { + return NULL; + } + auto seckey = std::unique_ptr<pgp_key_pkt_t>(new pgp_key_pkt_t(pubkey, false)); + if (!g23_parse_seckey(*seckey, raw.raw.data(), raw.raw.size(), password)) { + return NULL; + } + /* g10 has the same 'ecc' algo for ECDSA/ECDH/EDDSA. Probably should be better place to fix + * this. */ + seckey->alg = pubkey.alg; + seckey->material.alg = pubkey.material.alg; + return seckey.release(); +} + +static bool +copy_secret_fields(pgp_key_pkt_t &dst, const pgp_key_pkt_t &src) +{ + switch (src.alg) { + case PGP_PKA_DSA: + dst.material.dsa.x = src.material.dsa.x; + break; + case PGP_PKA_RSA: + case PGP_PKA_RSA_ENCRYPT_ONLY: + case PGP_PKA_RSA_SIGN_ONLY: + dst.material.rsa.d = src.material.rsa.d; + dst.material.rsa.p = src.material.rsa.p; + dst.material.rsa.q = src.material.rsa.q; + dst.material.rsa.u = src.material.rsa.u; + break; + case PGP_PKA_ELGAMAL: + case PGP_PKA_ELGAMAL_ENCRYPT_OR_SIGN: + dst.material.eg.x = src.material.eg.x; + break; + case PGP_PKA_ECDSA: + case PGP_PKA_ECDH: + case PGP_PKA_EDDSA: + dst.material.ec.x = src.material.ec.x; + break; + default: + RNP_LOG("Unsupported public key algorithm: %d", (int) src.alg); + return false; + } + + dst.material.secret = src.material.secret; + dst.sec_protection = src.sec_protection; + dst.tag = is_subkey_pkt(dst.tag) ? PGP_PKT_SECRET_SUBKEY : PGP_PKT_SECRET_KEY; + return true; +} + +bool +rnp_key_store_g10_from_src(rnp_key_store_t * key_store, + pgp_source_t * src, + const pgp_key_provider_t *key_provider) +{ + try { + /* read src to the memory */ + rnp::MemorySource memsrc(*src); + /* parse secret key: fills material and sec_protection only */ + pgp_key_pkt_t seckey; + if (!g23_parse_seckey(seckey, (uint8_t *) memsrc.memory(), memsrc.size(), NULL)) { + return false; + } + /* copy public key fields if any */ + pgp_key_t key; + if (key_provider) { + pgp_key_request_ctx_t req_ctx(PGP_OP_MERGE_INFO, false, PGP_KEY_SEARCH_GRIP); + if (!rnp_key_store_get_key_grip(&seckey.material, req_ctx.search.by.grip)) { + return false; + } + + const pgp_key_t *pubkey = pgp_request_key(key_provider, &req_ctx); + if (!pubkey) { + return false; + } + + /* public key packet has some more info then the secret part */ + key = pgp_key_t(*pubkey, true); + if (!copy_secret_fields(key.pkt(), seckey)) { + return false; + } + } else { + key.set_pkt(std::move(seckey)); + } + /* set rawpkt */ + key.set_rawpkt( + pgp_rawpacket_t((uint8_t *) memsrc.memory(), memsrc.size(), PGP_PKT_RESERVED)); + key.format = PGP_KEY_STORE_G10; + if (!rnp_key_store_add_key(key_store, &key)) { + return false; + } + return true; + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + return false; + } +} + +/* + * Write G10 S-exp to buffer + * + * Supported format: (1:a2:ab(3:asd1:a)) + */ +bool +gnupg_sexp_t::write(pgp_dest_t &dst) const noexcept +{ + bool res = false; + try { + std::ostringstream oss(std::ios_base::binary); + sexp_output_stream_t os(&oss); + print_canonical(&os); + const std::string &s = oss.str(); + const char * ss = s.c_str(); + dst_write(&dst, ss, s.size()); + res = (dst.werr == RNP_SUCCESS); + + } catch (...) { + } + + return res; +} + +void +gnupg_sexp_t::add_pubkey(const pgp_key_pkt_t &key) +{ + switch (key.alg) { + case PGP_PKA_DSA: + add("dsa"); + add_mpi("p", key.material.dsa.p); + add_mpi("q", key.material.dsa.q); + add_mpi("g", key.material.dsa.g); + add_mpi("y", key.material.dsa.y); + break; + case PGP_PKA_RSA_SIGN_ONLY: + case PGP_PKA_RSA_ENCRYPT_ONLY: + case PGP_PKA_RSA: + add("rsa"); + add_mpi("n", key.material.rsa.n); + add_mpi("e", key.material.rsa.e); + break; + case PGP_PKA_ELGAMAL: + add("elg"); + add_mpi("p", key.material.eg.p); + add_mpi("g", key.material.eg.g); + add_mpi("y", key.material.eg.y); + break; + case PGP_PKA_ECDSA: + case PGP_PKA_ECDH: + case PGP_PKA_EDDSA: + add("ecc"); + add_curve("curve", key.material.ec); + add_mpi("q", key.material.ec.p); + break; + default: + RNP_LOG("Unsupported public key algorithm: %d", (int) key.alg); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } +} + +void +gnupg_sexp_t::add_seckey(const pgp_key_pkt_t &key) +{ + switch (key.alg) { + case PGP_PKA_DSA: + add_mpi("x", key.material.dsa.x); + break; + case PGP_PKA_RSA_SIGN_ONLY: + case PGP_PKA_RSA_ENCRYPT_ONLY: + case PGP_PKA_RSA: + add_mpi("d", key.material.rsa.d); + add_mpi("p", key.material.rsa.p); + add_mpi("q", key.material.rsa.q); + add_mpi("u", key.material.rsa.u); + break; + case PGP_PKA_ELGAMAL: + add_mpi("x", key.material.eg.x); + break; + case PGP_PKA_ECDSA: + case PGP_PKA_ECDH: + case PGP_PKA_EDDSA: { + add_mpi("d", key.material.ec.x); + break; + } + default: + RNP_LOG("Unsupported public key algorithm: %d", (int) key.alg); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } +} + +rnp::secure_vector<uint8_t> +gnupg_sexp_t::write_padded(size_t padblock) const +{ + rnp::MemoryDest raw; + raw.set_secure(true); + + if (!write(raw.dst())) { + RNP_LOG("failed to serialize s_exp"); + throw rnp::rnp_exception(RNP_ERROR_BAD_STATE); + } + + // add padding! + size_t padding = padblock - raw.writeb() % padblock; + for (size_t i = 0; i < padding; i++) { + raw.write("X", 1); + } + if (raw.werr()) { + RNP_LOG("failed to write padding"); + throw rnp::rnp_exception(RNP_ERROR_BAD_STATE); + } + const uint8_t *mem = (uint8_t *) raw.memory(); + return rnp::secure_vector<uint8_t>(mem, mem + raw.writeb()); +} + +void +gnupg_sexp_t::add_protected_seckey(pgp_key_pkt_t & seckey, + const std::string & password, + rnp::SecurityContext &ctx) +{ + pgp_key_protection_t &prot = seckey.sec_protection; + if (prot.s2k.specifier != PGP_S2KS_ITERATED_AND_SALTED) { + RNP_LOG("Bad s2k specifier: %d", (int) prot.s2k.specifier); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + const format_info *format = + find_format(prot.symm_alg, prot.cipher_mode, prot.s2k.hash_alg); + if (!format) { + RNP_LOG("Unknown protection format."); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + + // randomize IV and salt + ctx.rng.get(prot.iv, sizeof(prot.iv)); + ctx.rng.get(prot.s2k.salt, sizeof(prot.s2k.salt)); + + // write seckey + gnupg_sexp_t raw_s_exp; + auto psub_s_exp = raw_s_exp.add_sub(); + psub_s_exp->add_seckey(seckey); + + // calculate hash + char protected_at[G10_PROTECTED_AT_SIZE + 1]; + uint8_t checksum[G10_SHA1_HASH_SIZE]; + // TODO: how critical is it if we have a skewed timestamp here due to y2k38 problem? + struct tm tm = {}; + rnp_gmtime(ctx.time(), tm); + strftime(protected_at, sizeof(protected_at), "%Y%m%dT%H%M%S", &tm); + if (!g10_calculated_hash(seckey, protected_at, checksum)) { + throw rnp::rnp_exception(RNP_ERROR_BAD_STATE); + } + + psub_s_exp = raw_s_exp.add_sub(); + psub_s_exp->add("hash"); + psub_s_exp->add("sha1"); + psub_s_exp->add(checksum, sizeof(checksum)); + + /* write raw secret key to the memory */ + rnp::secure_vector<uint8_t> rawkey = raw_s_exp.write_padded(format->cipher_block_size); + + /* derive encrypting key */ + unsigned keysize = pgp_key_size(prot.symm_alg); + if (!keysize) { + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + + rnp::secure_array<uint8_t, PGP_MAX_KEY_SIZE> derived_key; + if (pgp_s2k_iterated(format->hash_alg, + derived_key.data(), + keysize, + password.c_str(), + prot.s2k.salt, + prot.s2k.iterations)) { + RNP_LOG("s2k key derivation failed"); + throw rnp::rnp_exception(RNP_ERROR_BAD_STATE); + } + + /* encrypt raw key */ + std::unique_ptr<Cipher> enc( + Cipher::encryption(format->cipher, format->cipher_mode, 0, true)); + if (!enc || !enc->set_key(derived_key.data(), keysize) || + !enc->set_iv(prot.iv, format->iv_size)) { + throw rnp::rnp_exception(RNP_ERROR_BAD_STATE); + } + + size_t output_written, input_consumed; + std::vector<uint8_t> enckey(rawkey.size()); + + if (!enc->finish(enckey.data(), + enckey.size(), + &output_written, + rawkey.data(), + rawkey.size(), + &input_consumed)) { + RNP_LOG("Encryption failed"); + throw rnp::rnp_exception(RNP_ERROR_BAD_STATE); + } + + /* build s_exp with encrypted key */ + psub_s_exp = add_sub(); + psub_s_exp->add("protected"); + psub_s_exp->add(format->g10_type); + /* protection params: s2k, iv */ + auto psub_sub_s_exp = psub_s_exp->add_sub(); + /* s2k params: hash, salt, iterations */ + auto psub_sub_sub_s_exp = psub_sub_s_exp->add_sub(); + psub_sub_sub_s_exp->add("sha1"); + psub_sub_sub_s_exp->add(prot.s2k.salt, PGP_SALT_SIZE); + psub_sub_sub_s_exp->add(prot.s2k.iterations); + psub_sub_s_exp->add(prot.iv, format->iv_size); + /* encrypted key data itself */ + psub_s_exp->add(enckey.data(), enckey.size()); + /* protected-at */ + psub_s_exp = add_sub(); + psub_s_exp->add("protected-at"); + psub_s_exp->add((uint8_t *) protected_at, G10_PROTECTED_AT_SIZE); +} + +bool +g10_write_seckey(pgp_dest_t * dst, + pgp_key_pkt_t * seckey, + const char * password, + rnp::SecurityContext &ctx) +{ + bool is_protected = true; + + switch (seckey->sec_protection.s2k.usage) { + case PGP_S2KU_NONE: + is_protected = false; + break; + case PGP_S2KU_ENCRYPTED_AND_HASHED: + is_protected = true; + // TODO: these are forced for now, until openpgp-native is implemented + seckey->sec_protection.symm_alg = PGP_SA_AES_128; + seckey->sec_protection.cipher_mode = PGP_CIPHER_MODE_CBC; + seckey->sec_protection.s2k.hash_alg = PGP_HASH_SHA1; + break; + default: + RNP_LOG("unsupported s2k usage"); + return false; + } + + try { + gnupg_sexp_t s_exp; + s_exp.add(is_protected ? "protected-private-key" : "private-key"); + auto pkey = s_exp.add_sub(); + pkey->add_pubkey(*seckey); + + if (is_protected) { + pkey->add_protected_seckey(*seckey, password, ctx); + } else { + pkey->add_seckey(*seckey); + } + return s_exp.write(*dst) && !dst->werr; + } catch (const std::exception &e) { + RNP_LOG("Failed to write g10 key: %s", e.what()); + return false; + } +} + +static bool +g10_calculated_hash(const pgp_key_pkt_t &key, const char *protected_at, uint8_t *checksum) +{ + try { + /* populate s_exp */ + gnupg_sexp_t s_exp; + s_exp.add_pubkey(key); + s_exp.add_seckey(key); + auto s_sub_exp = s_exp.add_sub(); + s_sub_exp->add("protected-at"); + s_sub_exp->add((uint8_t *) protected_at, G10_PROTECTED_AT_SIZE); + /* write it to memdst */ + rnp::MemoryDest memdst; + memdst.set_secure(true); + if (!s_exp.write(memdst.dst())) { + RNP_LOG("Failed to write s_exp"); + return false; + } + auto hash = rnp::Hash::create(PGP_HASH_SHA1); + hash->add(memdst.memory(), memdst.writeb()); + hash->finish(checksum); + return true; + } catch (const std::exception &e) { + RNP_LOG("Failed to build s_exp: %s", e.what()); + return false; + } +} + +bool +rnp_key_store_gnupg_sexp_to_dst(pgp_key_t *key, pgp_dest_t *dest) +{ + if (key->format != PGP_KEY_STORE_G10) { + RNP_LOG("incorrect format: %d", key->format); + return false; + } + pgp_rawpacket_t &packet = key->rawpkt(); + dst_write(dest, packet.raw.data(), packet.raw.size()); + return dest->werr == RNP_SUCCESS; +} diff --git a/src/librekey/key_store_g10.h b/src/librekey/key_store_g10.h new file mode 100644 index 0000000..f770628 --- /dev/null +++ b/src/librekey/key_store_g10.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2017, [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 RIBOSE, INC. AND CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * 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. + */ + +#ifndef RNP_KEY_STORE_G10_H +#define RNP_KEY_STORE_G10_H + +#include <rekey/rnp_key_store.h> + +bool rnp_key_store_g10_from_src(rnp_key_store_t *, pgp_source_t *, const pgp_key_provider_t *); +bool rnp_key_store_gnupg_sexp_to_dst(pgp_key_t *, pgp_dest_t *); +bool g10_write_seckey(pgp_dest_t * dst, + pgp_key_pkt_t * seckey, + const char * password, + rnp::SecurityContext &ctx); +pgp_key_pkt_t *g10_decrypt_seckey(const pgp_rawpacket_t &raw, + const pgp_key_pkt_t & pubkey, + const char * password); + +#endif // RNP_KEY_STORE_G10_H diff --git a/src/librekey/key_store_kbx.cpp b/src/librekey/key_store_kbx.cpp new file mode 100644 index 0000000..bc504f6 --- /dev/null +++ b/src/librekey/key_store_kbx.cpp @@ -0,0 +1,706 @@ +/* + * Copyright (c) 2017-2022, [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 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. + */ + +#include <sys/types.h> +#ifdef HAVE_SYS_PARAM_H +#include <sys/param.h> +#else +#include "uniwin.h" +#endif +#include <string.h> +#include <stdint.h> +#include <time.h> +#include <inttypes.h> +#include <cassert> + +#include "key_store_pgp.h" +#include "key_store_kbx.h" +#include "pgp-key.h" +#include <librepgp/stream-sig.h> + +/* same limit with GnuPG 2.1 */ +#define BLOB_SIZE_LIMIT (5 * 1024 * 1024) +/* limit the number of keys/sigs/uids in the blob */ +#define BLOB_OBJ_LIMIT 0x8000 + +#define BLOB_HEADER_SIZE 0x5 +#define BLOB_FIRST_SIZE 0x20 +#define BLOB_KEY_SIZE 0x1C +#define BLOB_UID_SIZE 0x0C +#define BLOB_SIG_SIZE 0x04 +#define BLOB_VALIDITY_SIZE 0x10 + +uint8_t +kbx_blob_t::ru8(size_t idx) +{ + return image_[idx]; +} + +uint16_t +kbx_blob_t::ru16(size_t idx) +{ + return read_uint16(image_.data() + idx); +} + +uint32_t +kbx_blob_t::ru32(size_t idx) +{ + return read_uint32(image_.data() + idx); +} + +kbx_blob_t::kbx_blob_t(std::vector<uint8_t> &data) +{ + if (data.size() < BLOB_HEADER_SIZE) { + RNP_LOG("Too small KBX blob."); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + uint32_t len = read_uint32(data.data()); + if (len > BLOB_SIZE_LIMIT) { + RNP_LOG("Too large KBX blob."); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + if (len != data.size()) { + RNP_LOG("KBX blob size mismatch."); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + image_ = data; + type_ = (kbx_blob_type_t) ru8(4); +} + +bool +kbx_header_blob_t::parse() +{ + if (length() != BLOB_FIRST_SIZE) { + RNP_LOG("The first blob has wrong length: %" PRIu32 " but expected %d", + length(), + (int) BLOB_FIRST_SIZE); + return false; + } + + size_t idx = BLOB_HEADER_SIZE; + version_ = ru8(idx++); + if (version_ != 1) { + RNP_LOG("Wrong version, expect 1 but has %" PRIu8, version_); + return false; + } + + flags_ = ru16(idx); + idx += 2; + + // blob should contains a magic KBXf + if (memcmp(image_.data() + idx, "KBXf", 4)) { + RNP_LOG("The first blob hasn't got a KBXf magic string"); + return false; + } + idx += 4; + // RFU + idx += 4; + // File creation time + file_created_at_ = ru32(idx); + idx += 4; + // Duplicated? + file_created_at_ = ru32(idx); + // RFU +4 bytes + // RFU +4 bytes + return true; +} + +bool +kbx_pgp_blob_t::parse() +{ + if (image_.size() < 15 + BLOB_HEADER_SIZE) { + RNP_LOG("Too few data in the blob."); + return false; + } + + size_t idx = BLOB_HEADER_SIZE; + /* version */ + version_ = ru8(idx++); + if (version_ != 1) { + RNP_LOG("Wrong version: %" PRIu8, version_); + return false; + } + /* flags */ + flags_ = ru16(idx); + idx += 2; + /* keyblock offset */ + keyblock_offset_ = ru32(idx); + idx += 4; + /* keyblock length */ + keyblock_length_ = ru32(idx); + idx += 4; + + if ((keyblock_offset_ > image_.size()) || + (keyblock_offset_ > (UINT32_MAX - keyblock_length_)) || + (image_.size() < (keyblock_offset_ + keyblock_length_))) { + RNP_LOG("Wrong keyblock offset/length, blob size: %zu" + ", keyblock offset: %" PRIu32 ", length: %" PRIu32, + image_.size(), + keyblock_offset_, + keyblock_length_); + return false; + } + /* number of key blocks */ + size_t nkeys = ru16(idx); + idx += 2; + if (nkeys < 1) { + RNP_LOG("PGP blob should contains at least 1 key"); + return false; + } + if (nkeys > BLOB_OBJ_LIMIT) { + RNP_LOG("Too many keys in the PGP blob"); + return false; + } + + /* Size of the single key record */ + size_t keys_len = ru16(idx); + idx += 2; + if (keys_len < BLOB_KEY_SIZE) { + RNP_LOG( + "PGP blob needs %d bytes, but contains: %zu bytes", (int) BLOB_KEY_SIZE, keys_len); + return false; + } + + for (size_t i = 0; i < nkeys; i++) { + if (image_.size() - idx < keys_len) { + RNP_LOG("Too few bytes left for key blob"); + return false; + } + + kbx_pgp_key_t nkey = {}; + /* copy fingerprint */ + memcpy(nkey.fp, &image_[idx], 20); + idx += 20; + /* keyid offset */ + nkey.keyid_offset = ru32(idx); + idx += 4; + /* flags */ + nkey.flags = ru16(idx); + idx += 2; + /* RFU */ + idx += 2; + /* skip padding bytes if it existed */ + idx += keys_len - BLOB_KEY_SIZE; + keys_.push_back(std::move(nkey)); + } + + if (image_.size() - idx < 2) { + RNP_LOG("No data for sn_size"); + return false; + } + size_t sn_size = ru16(idx); + idx += 2; + + if (image_.size() - idx < sn_size) { + RNP_LOG("SN is %zu, while bytes left are %zu", sn_size, image_.size() - idx); + return false; + } + + if (sn_size) { + sn_ = {image_.begin() + idx, image_.begin() + idx + sn_size}; + idx += sn_size; + } + + if (image_.size() - idx < 4) { + RNP_LOG("Too few data for uids"); + return false; + } + size_t nuids = ru16(idx); + if (nuids > BLOB_OBJ_LIMIT) { + RNP_LOG("Too many uids in the PGP blob"); + return false; + } + + size_t uids_len = ru16(idx + 2); + idx += 4; + + if (uids_len < BLOB_UID_SIZE) { + RNP_LOG("Too few bytes for uid struct: %zu", uids_len); + return false; + } + + for (size_t i = 0; i < nuids; i++) { + if (image_.size() - idx < uids_len) { + RNP_LOG("Too few bytes to read uid struct."); + return false; + } + kbx_pgp_uid_t nuid = {}; + /* offset */ + nuid.offset = ru32(idx); + idx += 4; + /* length */ + nuid.length = ru32(idx); + idx += 4; + /* flags */ + nuid.flags = ru16(idx); + idx += 2; + /* validity */ + nuid.validity = ru8(idx); + idx++; + /* RFU */ + idx++; + // skip padding bytes if it existed + idx += uids_len - BLOB_UID_SIZE; + + uids_.push_back(std::move(nuid)); + } + + if (image_.size() - idx < 4) { + RNP_LOG("No data left for sigs"); + return false; + } + + size_t nsigs = ru16(idx); + if (nsigs > BLOB_OBJ_LIMIT) { + RNP_LOG("Too many sigs in the PGP blob"); + return false; + } + + size_t sigs_len = ru16(idx + 2); + idx += 4; + + if (sigs_len < BLOB_SIG_SIZE) { + RNP_LOG("Too small SIGN structure: %zu", uids_len); + return false; + } + + for (size_t i = 0; i < nsigs; i++) { + if (image_.size() - idx < sigs_len) { + RNP_LOG("Too few data for sig"); + return false; + } + + kbx_pgp_sig_t nsig = {}; + nsig.expired = ru32(idx); + idx += 4; + + // skip padding bytes if it existed + idx += (sigs_len - BLOB_SIG_SIZE); + + sigs_.push_back(nsig); + } + + if (image_.size() - idx < BLOB_VALIDITY_SIZE) { + RNP_LOG("Too few data for trust/validities"); + return false; + } + + ownertrust_ = ru8(idx); + idx++; + all_validity_ = ru8(idx); + idx++; + // RFU + idx += 2; + recheck_after_ = ru32(idx); + idx += 4; + latest_timestamp_ = ru32(idx); + idx += 4; + blob_created_at_ = ru32(idx); + // do not forget to idx += 4 on further expansion + + // here starts keyblock, UID and reserved space for future usage + + // Maybe we should add checksum verify but GnuPG never checked it + // Checksum is last 20 bytes of blob and it is SHA-1, if it invalid MD5 and starts from 4 + // zero it is MD5. + + return true; +} + +static std::unique_ptr<kbx_blob_t> +rnp_key_store_kbx_parse_blob(const uint8_t *image, size_t image_len) +{ + std::unique_ptr<kbx_blob_t> blob; + // a blob shouldn't be less of length + type + if (image_len < BLOB_HEADER_SIZE) { + RNP_LOG("Blob size is %zu but it shouldn't be less of header", image_len); + return blob; + } + + try { + std::vector<uint8_t> data(image, image + image_len); + kbx_blob_type_t type = (kbx_blob_type_t) image[4]; + + switch (type) { + case KBX_EMPTY_BLOB: + blob = std::unique_ptr<kbx_blob_t>(new kbx_blob_t(data)); + break; + case KBX_HEADER_BLOB: + blob = std::unique_ptr<kbx_blob_t>(new kbx_header_blob_t(data)); + break; + case KBX_PGP_BLOB: + blob = std::unique_ptr<kbx_blob_t>(new kbx_pgp_blob_t(data)); + break; + case KBX_X509_BLOB: + // current we doesn't parse X509 blob, so, keep it as is + blob = std::unique_ptr<kbx_blob_t>(new kbx_blob_t(data)); + break; + // unsupported blob type + default: + RNP_LOG("Unsupported blob type: %d", (int) type); + return blob; + } + + if (!blob->parse()) { + return NULL; + } + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + return NULL; + } + return blob; +} + +bool +rnp_key_store_kbx_from_src(rnp_key_store_t * key_store, + pgp_source_t * src, + const pgp_key_provider_t *key_provider) +{ + try { + rnp::MemorySource mem(*src); + size_t has_bytes = mem.size(); + uint8_t * buf = (uint8_t *) mem.memory(); + + while (has_bytes > 4) { + size_t blob_length = read_uint32(buf); + if (blob_length > BLOB_SIZE_LIMIT) { + RNP_LOG("Blob size is %zu bytes but limit is %d bytes", + blob_length, + (int) BLOB_SIZE_LIMIT); + return false; + } + if (blob_length < BLOB_HEADER_SIZE) { + RNP_LOG("Too small blob header size"); + return false; + } + if (has_bytes < blob_length) { + RNP_LOG("Blob have size %zu bytes but file contains only %zu bytes", + blob_length, + has_bytes); + return false; + } + auto blob = rnp_key_store_kbx_parse_blob(buf, blob_length); + if (!blob.get()) { + RNP_LOG("Failed to parse blob"); + return false; + } + kbx_blob_t *pblob = blob.get(); + key_store->blobs.push_back(std::move(blob)); + + if (pblob->type() == KBX_PGP_BLOB) { + // parse keyblock if it existed + kbx_pgp_blob_t &pgp_blob = dynamic_cast<kbx_pgp_blob_t &>(*pblob); + if (!pgp_blob.keyblock_length()) { + RNP_LOG("PGP blob have zero size"); + return false; + } + + rnp::MemorySource blsrc(pgp_blob.image().data() + pgp_blob.keyblock_offset(), + pgp_blob.keyblock_length(), + false); + if (rnp_key_store_pgp_read_from_src(key_store, &blsrc.src())) { + return false; + } + } + + has_bytes -= blob_length; + buf += blob_length; + } + return true; + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + return false; + } +} + +static bool +pbuf(pgp_dest_t *dst, const void *buf, size_t len) +{ + dst_write(dst, buf, len); + return dst->werr == RNP_SUCCESS; +} + +static bool +pu8(pgp_dest_t *dst, uint8_t p) +{ + return pbuf(dst, &p, 1); +} + +static bool +pu16(pgp_dest_t *dst, uint16_t f) +{ + uint8_t p[2]; + p[0] = (uint8_t)(f >> 8); + p[1] = (uint8_t) f; + return pbuf(dst, p, 2); +} + +static bool +pu32(pgp_dest_t *dst, uint32_t f) +{ + uint8_t p[4]; + STORE32BE(p, f); + return pbuf(dst, p, 4); +} + +static bool +rnp_key_store_kbx_write_header(rnp_key_store_t *key_store, pgp_dest_t *dst) +{ + uint16_t flags = 0; + uint32_t file_created_at = key_store->secctx.time(); + + if (!key_store->blobs.empty() && (key_store->blobs[0]->type() == KBX_HEADER_BLOB)) { + kbx_header_blob_t &blob = dynamic_cast<kbx_header_blob_t &>(*key_store->blobs[0]); + file_created_at = blob.file_created_at(); + } + + return !(!pu32(dst, BLOB_FIRST_SIZE) || !pu8(dst, KBX_HEADER_BLOB) || + !pu8(dst, 1) // version + || !pu16(dst, flags) || !pbuf(dst, "KBXf", 4) || !pu32(dst, 0) // RFU + || !pu32(dst, 0) // RFU + || !pu32(dst, file_created_at) || !pu32(dst, key_store->secctx.time()) || + !pu32(dst, 0)); // RFU +} + +static bool +rnp_key_store_kbx_write_pgp(rnp_key_store_t *key_store, pgp_key_t *key, pgp_dest_t *dst) +{ + rnp::MemoryDest mem(NULL, BLOB_SIZE_LIMIT); + + if (!pu32(&mem.dst(), 0)) { // length, we don't know length of blob yet, so it's 0 + return false; + } + + if (!pu8(&mem.dst(), KBX_PGP_BLOB) || !pu8(&mem.dst(), 1)) { // type, version + return false; + } + + if (!pu16(&mem.dst(), 0)) { // flags, not used by GnuPG + return false; + } + + if (!pu32(&mem.dst(), 0) || + !pu32(&mem.dst(), 0)) { // offset and length of keyblock, update later + return false; + } + + if (!pu16(&mem.dst(), 1 + key->subkey_count())) { // number of keys in keyblock + return false; + } + if (!pu16(&mem.dst(), 28)) { // size of key info structure) + return false; + } + + if (!pbuf(&mem.dst(), key->fp().fingerprint, PGP_FINGERPRINT_SIZE) || + !pu32(&mem.dst(), mem.writeb() - 8) || // offset to keyid (part of fpr for V4) + !pu16(&mem.dst(), 0) || // flags, not used by GnuPG + !pu16(&mem.dst(), 0)) { // RFU + return false; + } + + // same as above, for each subkey + std::vector<uint32_t> subkey_sig_expirations; + for (auto &sfp : key->subkey_fps()) { + pgp_key_t *subkey = rnp_key_store_get_key_by_fpr(key_store, sfp); + if (!subkey || !pbuf(&mem.dst(), subkey->fp().fingerprint, PGP_FINGERPRINT_SIZE) || + !pu32(&mem.dst(), mem.writeb() - 8) || // offset to keyid (part of fpr for V4) + !pu16(&mem.dst(), 0) || // flags, not used by GnuPG + !pu16(&mem.dst(), 0)) { // RFU + return false; + } + // load signature expirations while we're at it + for (size_t i = 0; i < subkey->sig_count(); i++) { + uint32_t expiration = subkey->get_sig(i).sig.key_expiration(); + subkey_sig_expirations.push_back(expiration); + } + } + + if (!pu16(&mem.dst(), 0)) { // Zero size of serial number + return false; + } + + // skip serial number + if (!pu16(&mem.dst(), key->uid_count()) || !pu16(&mem.dst(), 12)) { + return false; + } + + size_t uid_start = mem.writeb(); + for (size_t i = 0; i < key->uid_count(); i++) { + if (!pu32(&mem.dst(), 0) || + !pu32(&mem.dst(), 0)) { // UID offset and length, update when blob has done + return false; + } + + if (!pu16(&mem.dst(), 0)) { // flags, (not yet used) + return false; + } + + if (!pu8(&mem.dst(), 0) || !pu8(&mem.dst(), 0)) { // Validity & RFU + return false; + } + } + + if (!pu16(&mem.dst(), key->sig_count() + subkey_sig_expirations.size()) || + !pu16(&mem.dst(), 4)) { + return false; + } + + for (size_t i = 0; i < key->sig_count(); i++) { + if (!pu32(&mem.dst(), key->get_sig(i).sig.key_expiration())) { + return false; + } + } + for (auto &expiration : subkey_sig_expirations) { + if (!pu32(&mem.dst(), expiration)) { + return false; + } + } + + if (!pu8(&mem.dst(), 0) || + !pu8(&mem.dst(), 0)) { // Assigned ownertrust & All_Validity (not yet used) + return false; + } + + if (!pu16(&mem.dst(), 0) || !pu32(&mem.dst(), 0)) { // RFU & Recheck_after + return false; + } + + if (!pu32(&mem.dst(), key_store->secctx.time()) || + !pu32(&mem.dst(), key_store->secctx.time())) { // Latest timestamp && created + return false; + } + + if (!pu32(&mem.dst(), 0)) { // Size of reserved space + return false; + } + + // wrtite UID, we might redesign PGP write and use this information from keyblob + for (size_t i = 0; i < key->uid_count(); i++) { + const pgp_userid_t &uid = key->get_uid(i); + uint8_t * p = (uint8_t *) mem.memory() + uid_start + (12 * i); + /* store absolute uid offset in the output stream */ + uint32_t pt = mem.writeb() + dst->writeb; + STORE32BE(p, pt); + /* and uid length */ + pt = uid.str.size(); + STORE32BE(p + 4, pt); + /* uid data itself */ + if (!pbuf(&mem.dst(), uid.str.c_str(), pt)) { + return false; + } + } + + /* write keyblock and fix the offset/length */ + size_t key_start = mem.writeb(); + uint32_t pt = key_start; + uint8_t *p = (uint8_t *) mem.memory() + 8; + STORE32BE(p, pt); + + key->write(mem.dst()); + if (mem.werr()) { + return false; + } + + for (auto &sfp : key->subkey_fps()) { + const pgp_key_t *subkey = rnp_key_store_get_key_by_fpr(key_store, sfp); + if (!subkey) { + return false; + } + subkey->write(mem.dst()); + if (mem.werr()) { + return false; + } + } + + /* key blob length */ + pt = mem.writeb() - key_start; + p = (uint8_t *) mem.memory() + 12; + STORE32BE(p, pt); + + // fix the length of blob + pt = mem.writeb() + 20; + p = (uint8_t *) mem.memory(); + STORE32BE(p, pt); + + // checksum + auto hash = rnp::Hash::create(PGP_HASH_SHA1); + hash->add(mem.memory(), mem.writeb()); + uint8_t checksum[PGP_SHA1_HASH_SIZE]; + assert(hash->size() == sizeof(checksum)); + hash->finish(checksum); + + if (!(pbuf(&mem.dst(), checksum, PGP_SHA1_HASH_SIZE))) { + return false; + } + + /* finally write to the output */ + dst_write(dst, mem.memory(), mem.writeb()); + return !dst->werr; +} + +static bool +rnp_key_store_kbx_write_x509(rnp_key_store_t *key_store, pgp_dest_t *dst) +{ + for (auto &blob : key_store->blobs) { + if (blob->type() != KBX_X509_BLOB) { + continue; + } + if (!pbuf(dst, blob->image().data(), blob->length())) { + return false; + } + } + return true; +} + +bool +rnp_key_store_kbx_to_dst(rnp_key_store_t *key_store, pgp_dest_t *dst) +{ + try { + if (!rnp_key_store_kbx_write_header(key_store, dst)) { + RNP_LOG("Can't write KBX header"); + return false; + } + + for (auto &key : key_store->keys) { + if (!key.is_primary()) { + continue; + } + if (!rnp_key_store_kbx_write_pgp(key_store, &key, dst)) { + RNP_LOG("Can't write PGP blobs for key %p", &key); + return false; + } + } + + if (!rnp_key_store_kbx_write_x509(key_store, dst)) { + RNP_LOG("Can't write X509 blobs"); + return false; + } + return true; + } catch (const std::exception &e) { + RNP_LOG("Failed to write KBX store: %s", e.what()); + return false; + } +} diff --git a/src/librekey/key_store_kbx.h b/src/librekey/key_store_kbx.h new file mode 100644 index 0000000..68d725d --- /dev/null +++ b/src/librekey/key_store_kbx.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2017, [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 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. + */ + +#ifndef RNP_KEY_STORE_KBX_H +#define RNP_KEY_STORE_KBX_H + +#include <rekey/rnp_key_store.h> +#include "sec_profile.hpp" + +bool rnp_key_store_kbx_from_src(rnp_key_store_t *, pgp_source_t *, const pgp_key_provider_t *); +bool rnp_key_store_kbx_to_dst(rnp_key_store_t *, pgp_dest_t *); + +#endif // RNP_KEY_STORE_KBX_H diff --git a/src/librekey/key_store_pgp.cpp b/src/librekey/key_store_pgp.cpp new file mode 100644 index 0000000..6edc099 --- /dev/null +++ b/src/librekey/key_store_pgp.cpp @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2017-2020 [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. + */ +/* + * Copyright (c) 2005-2008 Nominet UK (www.nic.uk) + * All rights reserved. + * Contributors: Ben Laurie, Rachel Willmer. The Contributors have asserted + * their moral rights under the UK Copyright Design and Patents Act 1988 to + * be recorded as the authors of this copyright work. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if defined(__NetBSD__) +__COPYRIGHT("@(#) Copyright (c) 2009 The NetBSD Foundation, Inc. All rights reserved."); +__RCSID("$NetBSD: keyring.c,v 1.50 2011/06/25 00:37:44 agc Exp $"); +#endif + +#include <stdlib.h> +#include <string.h> + +#include <librepgp/stream-common.h> +#include <librepgp/stream-sig.h> +#include <librepgp/stream-packet.h> +#include <librepgp/stream-key.h> +#include "crypto/mem.h" + +#include "types.h" +#include "key_store_pgp.h" +#include "pgp-key.h" + +bool +rnp_key_store_add_transferable_subkey(rnp_key_store_t * keyring, + pgp_transferable_subkey_t *tskey, + pgp_key_t * pkey) +{ + try { + /* create subkey */ + pgp_key_t skey(*tskey, pkey); + /* add it to the storage */ + return rnp_key_store_add_key(keyring, &skey); + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + RNP_LOG_KEY_PKT("failed to create subkey %s", tskey->subkey); + RNP_LOG_KEY("primary key is %s", pkey); + return false; + } +} + +bool +rnp_key_store_add_transferable_key(rnp_key_store_t *keyring, pgp_transferable_key_t *tkey) +{ + pgp_key_t *addkey = NULL; + + /* create key from transferable key */ + try { + pgp_key_t key(*tkey); + /* temporary disable key validation */ + keyring->disable_validation = true; + /* add key to the storage before subkeys */ + addkey = rnp_key_store_add_key(keyring, &key); + } catch (const std::exception &e) { + keyring->disable_validation = false; + RNP_LOG_KEY_PKT("failed to add key %s", tkey->key); + return false; + } + + if (!addkey) { + keyring->disable_validation = false; + RNP_LOG("Failed to add key to key store."); + return false; + } + + /* add subkeys */ + for (auto &subkey : tkey->subkeys) { + if (!rnp_key_store_add_transferable_subkey(keyring, &subkey, addkey)) { + RNP_LOG("Failed to add subkey to key store."); + keyring->disable_validation = false; + goto error; + } + } + + /* now validate/refresh the whole key with subkeys */ + keyring->disable_validation = false; + addkey->revalidate(*keyring); + return true; +error: + /* during key addition all fields are copied so will be cleaned below */ + rnp_key_store_remove_key(keyring, addkey, false); + return false; +} + +rnp_result_t +rnp_key_store_pgp_read_key_from_src(rnp_key_store_t &keyring, + pgp_source_t & src, + bool skiperrors) +{ + pgp_transferable_key_t key; + rnp_result_t ret = process_pgp_key_auto(src, key, true, skiperrors); + + if (ret && (!skiperrors || (ret != RNP_ERROR_BAD_FORMAT))) { + return ret; + } + + /* check whether we have primary key */ + if (key.key.tag != PGP_PKT_RESERVED) { + return rnp_key_store_add_transferable_key(&keyring, &key) ? RNP_SUCCESS : + RNP_ERROR_BAD_STATE; + } + + /* we just skipped some unexpected packets and read nothing */ + if (key.subkeys.empty()) { + return RNP_SUCCESS; + } + + return rnp_key_store_add_transferable_subkey(&keyring, &key.subkeys.front(), NULL) ? + RNP_SUCCESS : + RNP_ERROR_BAD_STATE; +} + +rnp_result_t +rnp_key_store_pgp_read_from_src(rnp_key_store_t *keyring, pgp_source_t *src, bool skiperrors) +{ + /* check whether we have transferable subkey in source */ + if (is_subkey_pkt(stream_pkt_type(*src))) { + pgp_transferable_subkey_t tskey; + rnp_result_t ret = process_pgp_subkey(*src, tskey, skiperrors); + if (ret) { + return ret; + } + return rnp_key_store_add_transferable_subkey(keyring, &tskey, NULL) ? + RNP_SUCCESS : + RNP_ERROR_BAD_STATE; + } + + /* process armored or raw transferable key packets sequence(s) */ + try { + pgp_key_sequence_t keys; + rnp_result_t ret = process_pgp_keys(*src, keys, skiperrors); + if (ret) { + return ret; + } + for (auto &key : keys.keys) { + if (!rnp_key_store_add_transferable_key(keyring, &key)) { + return RNP_ERROR_BAD_STATE; + } + } + return RNP_SUCCESS; + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + return RNP_ERROR_BAD_PARAMETERS; + } +} + +std::vector<uint8_t> +rnp_key_to_vec(const pgp_key_t &key) +{ + rnp::MemoryDest dst; + key.write(dst.dst()); + return dst.to_vector(); +} + +static bool +do_write(rnp_key_store_t *key_store, pgp_dest_t *dst, bool secret) +{ + for (auto &key : key_store->keys) { + if (key.is_secret() != secret) { + continue; + } + // skip subkeys, they are written below (orphans are ignored) + if (!key.is_primary()) { + continue; + } + + if (key.format != PGP_KEY_STORE_GPG) { + RNP_LOG("incorrect format (conversions not supported): %d", key.format); + return false; + } + key.write(*dst); + if (dst->werr) { + return false; + } + for (auto &sfp : key.subkey_fps()) { + pgp_key_t *subkey = rnp_key_store_get_key_by_fpr(key_store, sfp); + if (!subkey) { + RNP_LOG("Missing subkey"); + continue; + } + subkey->write(*dst); + if (dst->werr) { + return false; + } + } + } + return true; +} + +bool +rnp_key_store_pgp_write_to_dst(rnp_key_store_t *key_store, pgp_dest_t *dst) +{ + // two separate passes (public keys, then secret keys) + return do_write(key_store, dst, false) && do_write(key_store, dst, true); +} diff --git a/src/librekey/key_store_pgp.h b/src/librekey/key_store_pgp.h new file mode 100644 index 0000000..d3dcd06 --- /dev/null +++ b/src/librekey/key_store_pgp.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2017-2020, [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. + */ +/* + * Copyright (c) 2005-2008 Nominet UK (www.nic.uk) + * All rights reserved. + * Contributors: Ben Laurie, Rachel Willmer. The Contributors have asserted + * their moral rights under the UK Copyright Design and Patents Act 1988 to + * be recorded as the authors of this copyright work. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. + * + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** \file + */ + +#ifndef KEY_STORE_PGP_H_ +#define KEY_STORE_PGP_H_ + +#include <rekey/rnp_key_store.h> +#include <librepgp/stream-common.h> +#include <librepgp/stream-key.h> + +/* Read the whole keyring from the src, processing all available keys or subkeys */ +rnp_result_t rnp_key_store_pgp_read_from_src(rnp_key_store_t *keyring, + pgp_source_t * src, + bool skiperrors = false); + +/* Read the first key or subkey from the src */ +rnp_result_t rnp_key_store_pgp_read_key_from_src(rnp_key_store_t &keyring, + pgp_source_t & src, + bool skiperrors = false); + +bool rnp_key_store_pgp_write_to_dst(rnp_key_store_t *key_store, pgp_dest_t *dst); + +bool rnp_key_store_add_transferable_subkey(rnp_key_store_t * keyring, + pgp_transferable_subkey_t *tskey, + pgp_key_t * pkey); + +bool rnp_key_store_add_transferable_key(rnp_key_store_t * keyring, + pgp_transferable_key_t *tkey); + +std::vector<uint8_t> rnp_key_to_vec(const pgp_key_t &key); + +#endif /* KEY_STORE_PGP_H_ */ diff --git a/src/librekey/rnp_key_store.cpp b/src/librekey/rnp_key_store.cpp new file mode 100644 index 0000000..002a51e --- /dev/null +++ b/src/librekey/rnp_key_store.cpp @@ -0,0 +1,803 @@ +/* + * Copyright (c) 2017-2022 [Ribose Inc](https://www.ribose.com). + * 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. + */ + +#include "config.h" +#include <sys/stat.h> +#include <sys/types.h> +#ifdef HAVE_SYS_PARAM_H +#include <sys/param.h> +#else +#include "uniwin.h" +#endif + +#include <assert.h> +#include <stdio.h> +#include <string.h> +#include <stdint.h> +#include <stdlib.h> +#include <dirent.h> +#include <errno.h> +#include <algorithm> +#include <stdexcept> + +#include <rekey/rnp_key_store.h> +#include <librepgp/stream-packet.h> + +#include "key_store_pgp.h" +#include "key_store_kbx.h" +#include "key_store_g10.h" +#include "kbx_blob.hpp" + +#include "pgp-key.h" +#include "fingerprint.h" +#include "crypto/hash.hpp" +#include "crypto/mem.h" +#include "file-utils.h" +#ifdef _WIN32 +#include "str-utils.h" +#endif + +bool +rnp_key_store_load_from_path(rnp_key_store_t * key_store, + const pgp_key_provider_t *key_provider) +{ + pgp_source_t src = {}; + + if (key_store->format == PGP_KEY_STORE_G10) { + auto dir = rnp_opendir(key_store->path.c_str()); + if (!dir) { + RNP_LOG( + "Can't open G10 directory %s: %s", key_store->path.c_str(), strerror(errno)); + return false; + } + + std::string dirname; + while (!((dirname = rnp_readdir_name(dir)).empty())) { + std::string path = rnp::path::append(key_store->path, dirname); + + if (init_file_src(&src, path.c_str())) { + RNP_LOG("failed to read file %s", path.c_str()); + continue; + } + // G10 may fail to read one file, so ignore it! + if (!rnp_key_store_g10_from_src(key_store, &src, key_provider)) { + RNP_LOG("Can't parse file: %s", path.c_str()); // TODO: %S ? + } + src_close(&src); + } + rnp_closedir(dir); + return true; + } + + /* init file source and load from it */ + if (init_file_src(&src, key_store->path.c_str())) { + RNP_LOG("failed to read file %s", key_store->path.c_str()); + return false; + } + + bool rc = rnp_key_store_load_from_src(key_store, &src, key_provider); + src_close(&src); + return rc; +} + +bool +rnp_key_store_load_from_src(rnp_key_store_t * key_store, + pgp_source_t * src, + const pgp_key_provider_t *key_provider) +{ + switch (key_store->format) { + case PGP_KEY_STORE_GPG: + return rnp_key_store_pgp_read_from_src(key_store, src) == RNP_SUCCESS; + case PGP_KEY_STORE_KBX: + return rnp_key_store_kbx_from_src(key_store, src, key_provider); + case PGP_KEY_STORE_G10: + return rnp_key_store_g10_from_src(key_store, src, key_provider); + default: + RNP_LOG("Unsupported load from memory for key-store format: %d", key_store->format); + } + + return false; +} + +bool +rnp_key_store_write_to_path(rnp_key_store_t *key_store) +{ + bool rc; + pgp_dest_t keydst = {}; + + /* write g10 key store to the directory */ + if (key_store->format == PGP_KEY_STORE_G10) { + char path[MAXPATHLEN]; + + struct stat path_stat; + if (rnp_stat(key_store->path.c_str(), &path_stat) != -1) { + if (!S_ISDIR(path_stat.st_mode)) { + RNP_LOG("G10 keystore should be a directory: %s", key_store->path.c_str()); + return false; + } + } else { + if (errno != ENOENT) { + RNP_LOG("stat(%s): %s", key_store->path.c_str(), strerror(errno)); + return false; + } + if (RNP_MKDIR(key_store->path.c_str(), S_IRWXU) != 0) { + RNP_LOG("mkdir(%s, S_IRWXU): %s", key_store->path.c_str(), strerror(errno)); + return false; + } + } + + for (auto &key : key_store->keys) { + char grip[PGP_FINGERPRINT_HEX_SIZE] = {0}; + rnp::hex_encode(key.grip().data(), key.grip().size(), grip, sizeof(grip)); + snprintf(path, sizeof(path), "%s/%s.key", key_store->path.c_str(), grip); + + if (init_tmpfile_dest(&keydst, path, true)) { + RNP_LOG("failed to create file"); + return false; + } + + if (!rnp_key_store_gnupg_sexp_to_dst(&key, &keydst)) { + RNP_LOG("failed to write key to file"); + dst_close(&keydst, true); + return false; + } + + rc = dst_finish(&keydst) == RNP_SUCCESS; + dst_close(&keydst, !rc); + + if (!rc) { + return false; + } + } + + return true; + } + + /* write kbx/gpg store to the single file */ + if (init_tmpfile_dest(&keydst, key_store->path.c_str(), true)) { + RNP_LOG("failed to create keystore file"); + return false; + } + + if (!rnp_key_store_write_to_dst(key_store, &keydst)) { + RNP_LOG("failed to write keys to file"); + dst_close(&keydst, true); + return false; + } + + rc = dst_finish(&keydst) == RNP_SUCCESS; + dst_close(&keydst, !rc); + return rc; +} + +bool +rnp_key_store_write_to_dst(rnp_key_store_t *key_store, pgp_dest_t *dst) +{ + switch (key_store->format) { + case PGP_KEY_STORE_GPG: + return rnp_key_store_pgp_write_to_dst(key_store, dst); + case PGP_KEY_STORE_KBX: + return rnp_key_store_kbx_to_dst(key_store, dst); + default: + RNP_LOG("Unsupported write to memory for key-store format: %d", key_store->format); + } + + return false; +} + +void +rnp_key_store_clear(rnp_key_store_t *keyring) +{ + keyring->keybyfp.clear(); + keyring->keys.clear(); + keyring->blobs.clear(); +} + +size_t +rnp_key_store_get_key_count(const rnp_key_store_t *keyring) +{ + return keyring->keys.size(); +} + +static bool +rnp_key_store_refresh_subkey_grips(rnp_key_store_t *keyring, pgp_key_t *key) +{ + if (key->is_subkey()) { + RNP_LOG("wrong argument"); + return false; + } + + for (auto &skey : keyring->keys) { + bool found = false; + + /* if we have primary_grip then we also added to subkey_grips */ + if (!skey.is_subkey() || skey.has_primary_fp()) { + continue; + } + + for (size_t i = 0; i < skey.sig_count(); i++) { + const pgp_subsig_t &subsig = skey.get_sig(i); + + if (subsig.sig.type() != PGP_SIG_SUBKEY) { + continue; + } + if (subsig.sig.has_keyfp() && (key->fp() == subsig.sig.keyfp())) { + found = true; + break; + } + if (subsig.sig.has_keyid() && (key->keyid() == subsig.sig.keyid())) { + found = true; + break; + } + } + + if (found) { + try { + key->link_subkey_fp(skey); + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + return false; + } + } + } + + return true; +} + +static pgp_key_t * +rnp_key_store_add_subkey(rnp_key_store_t *keyring, pgp_key_t *srckey, pgp_key_t *oldkey) +{ + pgp_key_t *primary = NULL; + if (oldkey) { + primary = rnp_key_store_get_primary_key(keyring, oldkey); + } + if (!primary) { + primary = rnp_key_store_get_primary_key(keyring, srckey); + } + + if (oldkey) { + /* check for the weird case when same subkey has different primary keys */ + if (srckey->has_primary_fp() && oldkey->has_primary_fp() && + (srckey->primary_fp() != oldkey->primary_fp())) { + RNP_LOG_KEY("Warning: different primary keys for subkey %s", srckey); + pgp_key_t *srcprim = rnp_key_store_get_key_by_fpr(keyring, srckey->primary_fp()); + if (srcprim && (srcprim != primary)) { + srcprim->remove_subkey_fp(srckey->fp()); + } + } + /* in case we already have key let's merge it in */ + if (!oldkey->merge(*srckey, primary)) { + RNP_LOG_KEY("failed to merge subkey %s", srckey); + RNP_LOG_KEY("primary key is %s", primary); + return NULL; + } + } else { + try { + keyring->keys.emplace_back(); + oldkey = &keyring->keys.back(); + keyring->keybyfp[srckey->fp()] = std::prev(keyring->keys.end()); + *oldkey = pgp_key_t(*srckey); + if (primary) { + primary->link_subkey_fp(*oldkey); + } + } catch (const std::exception &e) { + RNP_LOG_KEY("key %s copying failed", srckey); + RNP_LOG_KEY("primary key is %s", primary); + RNP_LOG("%s", e.what()); + if (oldkey) { + keyring->keys.pop_back(); + keyring->keybyfp.erase(srckey->fp()); + } + return NULL; + } + } + + /* validate all added keys if not disabled */ + if (!keyring->disable_validation && !oldkey->validated()) { + oldkey->validate_subkey(primary, keyring->secctx); + } + if (!oldkey->refresh_data(primary, keyring->secctx)) { + RNP_LOG_KEY("Failed to refresh subkey %s data", srckey); + RNP_LOG_KEY("primary key is %s", primary); + } + return oldkey; +} + +/* add a key to keyring */ +pgp_key_t * +rnp_key_store_add_key(rnp_key_store_t *keyring, pgp_key_t *srckey) +{ + assert(srckey->type() && srckey->version()); + pgp_key_t *added_key = rnp_key_store_get_key_by_fpr(keyring, srckey->fp()); + /* we cannot merge G10 keys - so just return it */ + if (added_key && (srckey->format == PGP_KEY_STORE_G10)) { + return added_key; + } + /* different processing for subkeys */ + if (srckey->is_subkey()) { + return rnp_key_store_add_subkey(keyring, srckey, added_key); + } + + if (added_key) { + if (!added_key->merge(*srckey)) { + RNP_LOG_KEY("failed to merge key %s", srckey); + return NULL; + } + } else { + try { + keyring->keys.emplace_back(); + added_key = &keyring->keys.back(); + keyring->keybyfp[srckey->fp()] = std::prev(keyring->keys.end()); + *added_key = pgp_key_t(*srckey); + /* primary key may be added after subkeys, so let's handle this case correctly */ + if (!rnp_key_store_refresh_subkey_grips(keyring, added_key)) { + RNP_LOG_KEY("failed to refresh subkey grips for %s", added_key); + } + } catch (const std::exception &e) { + RNP_LOG_KEY("key %s copying failed", srckey); + RNP_LOG("%s", e.what()); + if (added_key) { + keyring->keys.pop_back(); + keyring->keybyfp.erase(srckey->fp()); + } + return NULL; + } + } + + /* validate all added keys if not disabled or already validated */ + if (!keyring->disable_validation && !added_key->validated()) { + added_key->revalidate(*keyring); + } else if (!added_key->refresh_data(keyring->secctx)) { + RNP_LOG_KEY("Failed to refresh key %s data", srckey); + } + return added_key; +} + +pgp_key_t * +rnp_key_store_import_key(rnp_key_store_t * keyring, + pgp_key_t * srckey, + bool pubkey, + pgp_key_import_status_t *status) +{ + /* add public key */ + pgp_key_t *exkey = rnp_key_store_get_key_by_fpr(keyring, srckey->fp()); + size_t expackets = exkey ? exkey->rawpkt_count() : 0; + try { + pgp_key_t keycp(*srckey, pubkey); + keyring->disable_validation = true; + exkey = rnp_key_store_add_key(keyring, &keycp); + keyring->disable_validation = false; + if (!exkey) { + RNP_LOG("failed to add key to the keyring"); + return NULL; + } + bool changed = exkey->rawpkt_count() > expackets; + if (changed || !exkey->validated()) { + /* this will revalidated primary key with all subkeys */ + exkey->revalidate(*keyring); + } + if (status) { + *status = changed ? (expackets ? PGP_KEY_IMPORT_STATUS_UPDATED : + PGP_KEY_IMPORT_STATUS_NEW) : + PGP_KEY_IMPORT_STATUS_UNCHANGED; + } + return exkey; + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + keyring->disable_validation = false; + return NULL; + } +} + +pgp_key_t * +rnp_key_store_get_signer_key(rnp_key_store_t *store, const pgp_signature_t *sig) +{ + pgp_key_search_t search; + // prefer using the issuer fingerprint when available + if (sig->has_keyfp()) { + search.by.fingerprint = sig->keyfp(); + search.type = PGP_KEY_SEARCH_FINGERPRINT; + return rnp_key_store_search(store, &search, NULL); + } + // fall back to key id search + if (sig->has_keyid()) { + search.by.keyid = sig->keyid(); + search.type = PGP_KEY_SEARCH_KEYID; + return rnp_key_store_search(store, &search, NULL); + } + return NULL; +} + +static pgp_sig_import_status_t +rnp_key_store_import_subkey_signature(rnp_key_store_t * keyring, + pgp_key_t * key, + const pgp_signature_t *sig) +{ + if ((sig->type() != PGP_SIG_SUBKEY) && (sig->type() != PGP_SIG_REV_SUBKEY)) { + return PGP_SIG_IMPORT_STATUS_UNKNOWN; + } + pgp_key_t *primary = rnp_key_store_get_signer_key(keyring, sig); + if (!primary || !key->has_primary_fp()) { + RNP_LOG("No primary grip or primary key"); + return PGP_SIG_IMPORT_STATUS_UNKNOWN_KEY; + } + if (primary->fp() != key->primary_fp()) { + RNP_LOG("Wrong subkey signature's signer."); + return PGP_SIG_IMPORT_STATUS_UNKNOWN; + } + + try { + pgp_key_t tmpkey(key->pkt()); + tmpkey.add_sig(*sig); + if (!tmpkey.refresh_data(primary, keyring->secctx)) { + RNP_LOG("Failed to add signature to the key."); + return PGP_SIG_IMPORT_STATUS_UNKNOWN; + } + + size_t expackets = key->rawpkt_count(); + key = rnp_key_store_add_key(keyring, &tmpkey); + if (!key) { + RNP_LOG("Failed to add key with imported sig to the keyring"); + return PGP_SIG_IMPORT_STATUS_UNKNOWN; + } + return (key->rawpkt_count() > expackets) ? PGP_SIG_IMPORT_STATUS_NEW : + PGP_SIG_IMPORT_STATUS_UNCHANGED; + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + return PGP_SIG_IMPORT_STATUS_UNKNOWN; + } +} + +pgp_sig_import_status_t +rnp_key_store_import_key_signature(rnp_key_store_t * keyring, + pgp_key_t * key, + const pgp_signature_t *sig) +{ + if (key->is_subkey()) { + return rnp_key_store_import_subkey_signature(keyring, key, sig); + } + if ((sig->type() != PGP_SIG_DIRECT) && (sig->type() != PGP_SIG_REV_KEY)) { + RNP_LOG("Wrong signature type: %d", (int) sig->type()); + return PGP_SIG_IMPORT_STATUS_UNKNOWN; + } + + try { + pgp_key_t tmpkey(key->pkt()); + tmpkey.add_sig(*sig); + if (!tmpkey.refresh_data(keyring->secctx)) { + RNP_LOG("Failed to add signature to the key."); + return PGP_SIG_IMPORT_STATUS_UNKNOWN; + } + + size_t expackets = key->rawpkt_count(); + key = rnp_key_store_add_key(keyring, &tmpkey); + if (!key) { + RNP_LOG("Failed to add key with imported sig to the keyring"); + return PGP_SIG_IMPORT_STATUS_UNKNOWN; + } + return (key->rawpkt_count() > expackets) ? PGP_SIG_IMPORT_STATUS_NEW : + PGP_SIG_IMPORT_STATUS_UNCHANGED; + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + return PGP_SIG_IMPORT_STATUS_UNKNOWN; + } +} + +pgp_key_t * +rnp_key_store_import_signature(rnp_key_store_t * keyring, + const pgp_signature_t * sig, + pgp_sig_import_status_t *status) +{ + pgp_sig_import_status_t tmp_status = PGP_SIG_IMPORT_STATUS_UNKNOWN; + if (!status) { + status = &tmp_status; + } + *status = PGP_SIG_IMPORT_STATUS_UNKNOWN; + + /* we support only direct-key and key revocation signatures here */ + if ((sig->type() != PGP_SIG_DIRECT) && (sig->type() != PGP_SIG_REV_KEY)) { + return NULL; + } + + pgp_key_t *res_key = rnp_key_store_get_signer_key(keyring, sig); + if (!res_key || !res_key->is_primary()) { + *status = PGP_SIG_IMPORT_STATUS_UNKNOWN_KEY; + return NULL; + } + *status = rnp_key_store_import_key_signature(keyring, res_key, sig); + return res_key; +} + +bool +rnp_key_store_remove_key(rnp_key_store_t *keyring, const pgp_key_t *key, bool subkeys) +{ + auto it = keyring->keybyfp.find(key->fp()); + if (it == keyring->keybyfp.end()) { + return false; + } + + /* cleanup primary_grip (or subkey)/subkey_grips */ + if (key->is_primary() && key->subkey_count()) { + for (size_t i = 0; i < key->subkey_count(); i++) { + auto it = keyring->keybyfp.find(key->get_subkey_fp(i)); + if (it == keyring->keybyfp.end()) { + continue; + } + /* if subkeys are deleted then no need to update grips */ + if (subkeys) { + keyring->keys.erase(it->second); + keyring->keybyfp.erase(it); + continue; + } + it->second->unset_primary_fp(); + } + } + if (key->is_subkey() && key->has_primary_fp()) { + pgp_key_t *primary = rnp_key_store_get_primary_key(keyring, key); + if (primary) { + primary->remove_subkey_fp(key->fp()); + } + } + + keyring->keys.erase(it->second); + keyring->keybyfp.erase(it); + return true; +} + +const pgp_key_t * +rnp_key_store_get_key_by_fpr(const rnp_key_store_t *keyring, const pgp_fingerprint_t &fpr) +{ + auto it = keyring->keybyfp.find(fpr); + if (it == keyring->keybyfp.end()) { + return NULL; + } + return &*it->second; +} + +pgp_key_t * +rnp_key_store_get_key_by_fpr(rnp_key_store_t *keyring, const pgp_fingerprint_t &fpr) +{ + auto it = keyring->keybyfp.find(fpr); + if (it == keyring->keybyfp.end()) { + return NULL; + } + return &*it->second; +} + +pgp_key_t * +rnp_key_store_get_primary_key(rnp_key_store_t *keyring, const pgp_key_t *subkey) +{ + if (!subkey->is_subkey()) { + return NULL; + } + + if (subkey->has_primary_fp()) { + pgp_key_t *primary = rnp_key_store_get_key_by_fpr(keyring, subkey->primary_fp()); + return primary && primary->is_primary() ? primary : NULL; + } + + for (size_t i = 0; i < subkey->sig_count(); i++) { + const pgp_subsig_t &subsig = subkey->get_sig(i); + if (subsig.sig.type() != PGP_SIG_SUBKEY) { + continue; + } + + pgp_key_t *primary = rnp_key_store_get_signer_key(keyring, &subsig.sig); + if (primary && primary->is_primary()) { + return primary; + } + } + return NULL; +} + +static void +grip_hash_mpi(rnp::Hash &hash, const pgp_mpi_t &val, const char name, bool lzero = true) +{ + size_t len = mpi_bytes(&val); + size_t idx = 0; + for (idx = 0; (idx < len) && !val.mpi[idx]; idx++) + ; + + if (name) { + size_t hlen = idx >= len ? 0 : len - idx; + if ((len > idx) && lzero && (val.mpi[idx] & 0x80)) { + hlen++; + } + + char buf[20] = {0}; + snprintf(buf, sizeof(buf), "(1:%c%zu:", name, hlen); + hash.add(buf, strlen(buf)); + } + + if (idx < len) { + /* gcrypt prepends mpis with zero if higher bit is set */ + if (lzero && (val.mpi[idx] & 0x80)) { + uint8_t zero = 0; + hash.add(&zero, 1); + } + hash.add(val.mpi + idx, len - idx); + } + if (name) { + hash.add(")", 1); + } +} + +static void +grip_hash_ecc_hex(rnp::Hash &hash, const char *hex, char name) +{ + pgp_mpi_t mpi = {}; + mpi.len = rnp::hex_decode(hex, mpi.mpi, sizeof(mpi.mpi)); + if (!mpi.len) { + RNP_LOG("wrong hex mpi"); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + + /* libgcrypt doesn't add leading zero when hashes ecc mpis */ + return grip_hash_mpi(hash, mpi, name, false); +} + +static void +grip_hash_ec(rnp::Hash &hash, const pgp_ec_key_t &key) +{ + const ec_curve_desc_t *desc = get_curve_desc(key.curve); + if (!desc) { + RNP_LOG("unknown curve %d", (int) key.curve); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + + /* build uncompressed point from gx and gy */ + pgp_mpi_t g = {}; + g.mpi[0] = 0x04; + g.len = 1; + size_t len = rnp::hex_decode(desc->gx, g.mpi + g.len, sizeof(g.mpi) - g.len); + if (!len) { + RNP_LOG("wrong x mpi"); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + g.len += len; + len = rnp::hex_decode(desc->gy, g.mpi + g.len, sizeof(g.mpi) - g.len); + if (!len) { + RNP_LOG("wrong y mpi"); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + g.len += len; + + /* p, a, b, g, n, q */ + grip_hash_ecc_hex(hash, desc->p, 'p'); + grip_hash_ecc_hex(hash, desc->a, 'a'); + grip_hash_ecc_hex(hash, desc->b, 'b'); + grip_hash_mpi(hash, g, 'g', false); + grip_hash_ecc_hex(hash, desc->n, 'n'); + + if ((key.curve == PGP_CURVE_ED25519) || (key.curve == PGP_CURVE_25519)) { + if (g.len < 1) { + RNP_LOG("wrong 25519 p"); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + g.len = key.p.len - 1; + memcpy(g.mpi, key.p.mpi + 1, g.len); + grip_hash_mpi(hash, g, 'q', false); + } else { + grip_hash_mpi(hash, key.p, 'q', false); + } +} + +/* keygrip is subjectKeyHash from pkcs#15 for RSA. */ +bool +rnp_key_store_get_key_grip(const pgp_key_material_t *key, pgp_key_grip_t &grip) +{ + try { + auto hash = rnp::Hash::create(PGP_HASH_SHA1); + switch (key->alg) { + case PGP_PKA_RSA: + case PGP_PKA_RSA_SIGN_ONLY: + case PGP_PKA_RSA_ENCRYPT_ONLY: + grip_hash_mpi(*hash, key->rsa.n, '\0'); + break; + case PGP_PKA_DSA: + grip_hash_mpi(*hash, key->dsa.p, 'p'); + grip_hash_mpi(*hash, key->dsa.q, 'q'); + grip_hash_mpi(*hash, key->dsa.g, 'g'); + grip_hash_mpi(*hash, key->dsa.y, 'y'); + break; + case PGP_PKA_ELGAMAL: + case PGP_PKA_ELGAMAL_ENCRYPT_OR_SIGN: + grip_hash_mpi(*hash, key->eg.p, 'p'); + grip_hash_mpi(*hash, key->eg.g, 'g'); + grip_hash_mpi(*hash, key->eg.y, 'y'); + break; + case PGP_PKA_ECDH: + case PGP_PKA_ECDSA: + case PGP_PKA_EDDSA: + case PGP_PKA_SM2: + grip_hash_ec(*hash, key->ec); + break; + default: + RNP_LOG("unsupported public-key algorithm %d", (int) key->alg); + return false; + } + return hash->finish(grip.data()) == grip.size(); + } catch (const std::exception &e) { + RNP_LOG("Grip calculation failed: %s", e.what()); + return false; + } +} + +pgp_key_t * +rnp_key_store_search(rnp_key_store_t * keyring, + const pgp_key_search_t *search, + pgp_key_t * after) +{ + // since keys are distinguished by fingerprint then just do map lookup + if (search->type == PGP_KEY_SEARCH_FINGERPRINT) { + pgp_key_t *key = rnp_key_store_get_key_by_fpr(keyring, search->by.fingerprint); + if (after && (after != key)) { + RNP_LOG("searching with invalid after param"); + return NULL; + } + // return NULL if after is specified + return after ? NULL : key; + } + + // if after is provided, make sure it is a member of the appropriate list + auto it = + std::find_if(keyring->keys.begin(), keyring->keys.end(), [after](const pgp_key_t &key) { + return !after || (after == &key); + }); + if (after && (it == keyring->keys.end())) { + RNP_LOG("searching with non-keyrings after param"); + return NULL; + } + if (after) { + it = std::next(it); + } + it = std::find_if(it, keyring->keys.end(), [search](const pgp_key_t &key) { + return rnp_key_matches_search(&key, search); + }); + return (it == keyring->keys.end()) ? NULL : &(*it); +} + +rnp_key_store_t::rnp_key_store_t(pgp_key_store_format_t _format, + const std::string & _path, + rnp::SecurityContext & ctx) + : secctx(ctx) +{ + if (_format == PGP_KEY_STORE_UNKNOWN) { + RNP_LOG("Invalid key store format"); + throw std::invalid_argument("format"); + } + format = _format; + path = _path; +} + +rnp_key_store_t::~rnp_key_store_t() +{ + rnp_key_store_clear(this); +} |