diff options
Diffstat (limited to 'src/librekey/key_store_g10.cpp')
-rw-r--r-- | src/librekey/key_store_g10.cpp | 1243 |
1 files changed, 1243 insertions, 0 deletions
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; +} |