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