diff options
Diffstat (limited to 'src/librepgp/stream-armor.cpp')
-rw-r--r-- | src/librepgp/stream-armor.cpp | 1287 |
1 files changed, 1287 insertions, 0 deletions
diff --git a/src/librepgp/stream-armor.cpp b/src/librepgp/stream-armor.cpp new file mode 100644 index 0000000..669c305 --- /dev/null +++ b/src/librepgp/stream-armor.cpp @@ -0,0 +1,1287 @@ +/* + * 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 OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include <stdlib.h> +#include <stdio.h> +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#else +#include "uniwin.h" +#endif +#include <string.h> +#include <algorithm> +#include "stream-def.h" +#include "stream-armor.h" +#include "stream-packet.h" +#include "str-utils.h" +#include "crypto/hash.hpp" +#include "utils.h" + +#define ARMORED_BLOCK_SIZE (4096) +#define ARMORED_PEEK_BUF_SIZE 1024 +#define ARMORED_MIN_LINE_LENGTH (16) +#define ARMORED_MAX_LINE_LENGTH (76) + +typedef struct pgp_source_armored_param_t { + pgp_source_t * readsrc; /* source to read from */ + pgp_armored_msg_t type; /* type of the message */ + char * armorhdr; /* armor header */ + char * version; /* Version: header if any */ + char * comment; /* Comment: header if any */ + char * hash; /* Hash: header if any */ + char * charset; /* Charset: header if any */ + uint8_t rest[ARMORED_BLOCK_SIZE]; /* unread decoded bytes, makes implementation easier */ + unsigned restlen; /* number of bytes in rest */ + unsigned restpos; /* index of first unread byte in rest, restpos <= restlen */ + uint8_t brest[3]; /* decoded 6-bit tail bytes */ + unsigned brestlen; /* number of bytes in brest */ + bool eofb64; /* end of base64 stream reached */ + uint8_t readcrc[3]; /* crc-24 from the armored data */ + bool has_crc; /* message contains CRC line */ + std::unique_ptr<rnp::CRC24> crc_ctx; /* CTX used to calculate CRC */ + bool noheaders; /* only base64 data, no headers */ +} pgp_source_armored_param_t; + +typedef struct pgp_dest_armored_param_t { + pgp_dest_t * writedst; + pgp_armored_msg_t type; /* type of the message */ + char eol[2]; /* end of line, all non-zeroes are written */ + unsigned lout; /* chars written in current line */ + unsigned llen; /* length of the base64 line, defaults to 76 as per RFC */ + uint8_t tail[2]; /* bytes which didn't fit into 3-byte boundary */ + unsigned tailc; /* number of bytes in tail */ + std::unique_ptr<rnp::CRC24> crc_ctx; /* CTX used to calculate CRC */ +} pgp_dest_armored_param_t; + +/* + Table for base64 lookups: + 0xff - wrong character, + 0xfe - '=' + 0xfd - eol/whitespace, + 0..0x3f - represented 6-bit number +*/ +static const uint8_t B64DEC[256] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xfd, 0xff, 0xff, 0xfd, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3e, 0xff, + 0xff, 0xff, 0x3f, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0xff, 0xff, + 0xff, 0xfe, 0xff, 0xff, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, + 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff}; + +static bool +armor_read_padding(pgp_source_armored_param_t *param, size_t *read) +{ + char st[64]; + size_t stlen = 0; + + if (!src_peek_line(param->readsrc, st, 64, &stlen)) { + return false; + } + + if ((stlen == 1) || (stlen == 2)) { + if ((st[0] != CH_EQ) || ((stlen == 2) && (st[1] != CH_EQ))) { + return false; + } + + *read = stlen; + src_skip(param->readsrc, stlen); + return src_skip_eol(param->readsrc); + } else if (stlen == 5) { + *read = 0; + return true; + } else if ((stlen > 5) && !memcmp(st, ST_DASHES, 5)) { + /* case with absent crc and 3-byte last chunk */ + *read = 0; + return true; + } + return false; +} + +static bool +base64_read_padding(pgp_source_armored_param_t *param, size_t *read) +{ + char pad[16]; + size_t padlen = sizeof(pad); + + /* we would allow arbitrary number of whitespaces/eols after the padding */ + if (!src_read(param->readsrc, pad, padlen, &padlen)) { + return false; + } + /* strip trailing whitespaces */ + while (padlen && (B64DEC[(int) pad[padlen - 1]] == 0xfd)) { + padlen--; + } + /* check for '=' */ + for (size_t i = 0; i < padlen; i++) { + if (pad[i] != CH_EQ) { + RNP_LOG("wrong base64 padding: %.*s", (int) padlen, pad); + return false; + } + } + if (padlen > 2) { + RNP_LOG("wrong base64 padding length %zu.", padlen); + return false; + } + if (!src_eof(param->readsrc)) { + RNP_LOG("warning: extra data after the base64 stream."); + } + *read = padlen; + return true; +} + +static bool +armor_read_crc(pgp_source_t *src) +{ + uint8_t dec[4] = {0}; + char crc[8] = {0}; + size_t clen = 0; + pgp_source_armored_param_t *param = (pgp_source_armored_param_t *) src->param; + + if (!src_peek_line(param->readsrc, crc, sizeof(crc), &clen)) { + return false; + } + + if ((clen != 5) || (crc[0] != CH_EQ)) { + return false; + } + + for (int i = 0; i < 4; i++) { + if ((dec[i] = B64DEC[(uint8_t) crc[i + 1]]) >= 64) { + return false; + } + } + + param->readcrc[0] = (dec[0] << 2) | ((dec[1] >> 4) & 0x0F); + param->readcrc[1] = (dec[1] << 4) | ((dec[2] >> 2) & 0x0F); + param->readcrc[2] = (dec[2] << 6) | dec[3]; + + param->has_crc = true; + + src_skip(param->readsrc, 5); + return src_skip_eol(param->readsrc); +} + +static bool +armor_skip_chars(pgp_source_t *src, const char *chars) +{ + uint8_t ch; + size_t read; + + do { + bool found = false; + if (!src_peek(src, &ch, 1, &read)) { + return false; + } + if (!read) { + /* return true only if there is no underlying read error */ + return true; + } + for (const char *chptr = chars; *chptr; chptr++) { + if (ch == *chptr) { + src_skip(src, 1); + found = true; + break; + } + } + if (!found) { + break; + } + } while (1); + + return true; +} + +static bool +armor_read_trailer(pgp_source_t *src) +{ + char st[64]; + char str[64]; + size_t stlen; + pgp_source_armored_param_t *param = (pgp_source_armored_param_t *) src->param; + + if (!armor_skip_chars(param->readsrc, "\r\n")) { + return false; + } + + stlen = strlen(param->armorhdr); + if ((stlen > 5) && (stlen + 8 + 1 <= sizeof(st))) { + memcpy(st, ST_ARMOR_END, 8); /* 8 here is mandatory */ + memcpy(st + 8, param->armorhdr + 5, stlen - 5); + memcpy(st + stlen + 3, ST_DASHES, 5); + stlen += 8; + } else { + RNP_LOG("Internal error"); + return false; + } + if (!src_peek_eq(param->readsrc, str, stlen) || strncmp(str, st, stlen)) { + return false; + } + src_skip(param->readsrc, stlen); + (void) armor_skip_chars(param->readsrc, "\t "); + (void) src_skip_eol(param->readsrc); + return true; +} + +static bool +armored_update_crc(pgp_source_armored_param_t *param, + const void * buf, + size_t len, + bool finish = false) +{ + if (param->noheaders) { + return true; + } + try { + param->crc_ctx->add(buf, len); + if (!finish) { + return true; + } + auto crc = param->crc_ctx->finish(); + if (param->has_crc && memcmp(param->readcrc, crc.data(), 3)) { + RNP_LOG("Warning: CRC mismatch"); + } + return true; + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + return false; + } +} + +static bool +armored_src_read(pgp_source_t *src, void *buf, size_t len, size_t *readres) +{ + pgp_source_armored_param_t *param = (pgp_source_armored_param_t *) src->param; + uint8_t b64buf[ARMORED_BLOCK_SIZE]; /* input base64 data with spaces and so on */ + uint8_t decbuf[ARMORED_BLOCK_SIZE + 4]; /* decoded 6-bit values */ + uint8_t *bufptr = (uint8_t *) buf; /* for better readability below */ + uint8_t *bptr, *bend; /* pointer to input data in b64buf */ + uint8_t *dptr, *dend, *pend; /* pointers to decoded data in decbuf: working pointer, last + available byte, last byte to process */ + uint8_t bval; + uint32_t b24; + size_t read = 0; + size_t left = len; + size_t eqcount = 0; /* number of '=' at the end of base64 stream */ + + if (!param) { + return false; + } + + /* checking whether there are some decoded bytes */ + if (param->restpos < param->restlen) { + if (param->restlen - param->restpos >= len) { + memcpy(bufptr, ¶m->rest[param->restpos], len); + param->restpos += len; + try { + param->crc_ctx->add(bufptr, len); + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + return false; + } + *readres = len; + return true; + } else { + left = len - (param->restlen - param->restpos); + memcpy(bufptr, ¶m->rest[param->restpos], len - left); + param->restpos = param->restlen = 0; + bufptr += len - left; + } + } + + if (param->eofb64) { + *readres = len - left; + return true; + } + + memcpy(decbuf, param->brest, param->brestlen); + dend = decbuf + param->brestlen; + + do { + if (!src_peek(param->readsrc, b64buf, sizeof(b64buf), &read)) { + return false; + } + if (!read) { + RNP_LOG("premature end of armored input"); + return false; + } + + dptr = dend; + bptr = b64buf; + bend = b64buf + read; + /* checking input data, stripping away whitespaces, checking for end of the b64 data */ + while (bptr < bend) { + if ((bval = B64DEC[*(bptr++)]) < 64) { + *(dptr++) = bval; + } else if (bval == 0xfe) { + /* '=' means the base64 padding or the beginning of checksum */ + param->eofb64 = true; + break; + } else if (bval == 0xff) { + auto ch = *(bptr - 1); + /* OpenPGP message headers without the crc and without trailing = */ + if ((ch == CH_DASH) && !param->noheaders) { + param->eofb64 = true; + break; + } + RNP_LOG("wrong base64 character 0x%02hhX", ch); + return false; + } + } + + dend = dptr; + dptr = decbuf; + /* Processing full 4s which will go directly to the buf. + After this left < 3 or decbuf has < 4 bytes */ + if ((size_t)(dend - dptr) / 4 * 3 < left) { + pend = decbuf + (dend - dptr) / 4 * 4; + left -= (dend - dptr) / 4 * 3; + } else { + pend = decbuf + (left / 3) * 4; + left -= left / 3 * 3; + } + + /* this one would the most performance-consuming part for large chunks */ + while (dptr < pend) { + b24 = *dptr++ << 18; + b24 |= *dptr++ << 12; + b24 |= *dptr++ << 6; + b24 |= *dptr++; + *bufptr++ = b24 >> 16; + *bufptr++ = b24 >> 8; + *bufptr++ = b24 & 0xff; + } + + /* moving rest to the beginning of decbuf */ + memmove(decbuf, dptr, dend - dptr); + dend = decbuf + (dend - dptr); + + /* skip already processed data */ + if (!param->eofb64) { + /* all input is base64 data or eol/spaces, so skipping it */ + src_skip(param->readsrc, read); + /* check for eof for base64-encoded data without headers */ + if (param->noheaders && src_eof(param->readsrc)) { + src_skip(param->readsrc, read); + param->eofb64 = true; + } else { + continue; + } + } else { + /* '=' reached, bptr points on it */ + src_skip(param->readsrc, bptr - b64buf - 1); + } + + /* end of base64 data */ + if (param->noheaders) { + if (!base64_read_padding(param, &eqcount)) { + return false; + } + break; + } + /* reading b64 padding if any */ + if (!armor_read_padding(param, &eqcount)) { + RNP_LOG("wrong padding"); + return false; + } + /* reading crc */ + if (!armor_read_crc(src)) { + RNP_LOG("Warning: missing or malformed CRC line"); + } + /* reading armor trailing line */ + if (!armor_read_trailer(src)) { + RNP_LOG("wrong armor trailer"); + return false; + } + break; + } while (left >= 3); + + /* process bytes left in decbuf */ + + dptr = decbuf; + pend = decbuf + (dend - decbuf) / 4 * 4; + bptr = param->rest; + while (dptr < pend) { + b24 = *dptr++ << 18; + b24 |= *dptr++ << 12; + b24 |= *dptr++ << 6; + b24 |= *dptr++; + *bptr++ = b24 >> 16; + *bptr++ = b24 >> 8; + *bptr++ = b24 & 0xff; + } + + if (!armored_update_crc(param, buf, bufptr - (uint8_t *) buf)) { + return false; + } + + if (param->eofb64) { + if ((dend - dptr + eqcount) % 4 != 0) { + RNP_LOG("wrong b64 padding"); + return false; + } + + if (eqcount == 1) { + b24 = (*dptr << 10) | (*(dptr + 1) << 4) | (*(dptr + 2) >> 2); + *bptr++ = b24 >> 8; + *bptr++ = b24 & 0xff; + } else if (eqcount == 2) { + *bptr++ = (*dptr << 2) | (*(dptr + 1) >> 4); + } + + /* Calculate CRC after reading whole input stream */ + if (!armored_update_crc(param, param->rest, bptr - param->rest, true)) { + return false; + } + } else { + /* few bytes which do not fit to 4 boundary */ + for (int i = 0; i < dend - dptr; i++) { + param->brest[i] = *(dptr + i); + } + param->brestlen = dend - dptr; + } + + param->restlen = bptr - param->rest; + + /* check whether we have some bytes to add */ + if ((left > 0) && (param->restlen > 0)) { + read = left > param->restlen ? param->restlen : left; + memcpy(bufptr, param->rest, read); + if (!param->eofb64 && !armored_update_crc(param, bufptr, read)) { + return false; + } + left -= read; + param->restpos += read; + } + + *readres = len - left; + return true; +} + +static void +armored_src_close(pgp_source_t *src) +{ + pgp_source_armored_param_t *param = (pgp_source_armored_param_t *) src->param; + + if (param) { + free(param->armorhdr); + free(param->version); + free(param->comment); + free(param->hash); + free(param->charset); + delete param; + src->param = NULL; + } +} + +/** @brief finds armor header position in the buffer, returning beginning of header or NULL. + * hdrlen will contain the length of the header + **/ +static const char * +find_armor_header(const char *buf, size_t len, size_t *hdrlen) +{ + int st = -1; + + for (unsigned i = 0; i < len - 10; i++) { + if ((buf[i] == CH_DASH) && !strncmp(&buf[i + 1], ST_DASHES, 4)) { + st = i; + break; + } + } + + if (st < 0) { + return NULL; + } + + for (unsigned i = st + 5; i <= len - 5; i++) { + if ((buf[i] == CH_DASH) && !strncmp(&buf[i + 1], ST_DASHES, 4)) { + *hdrlen = i + 5 - st; + return &buf[st]; + } + } + + return NULL; +} + +static bool +str_equals(const char *str, size_t len, const char *another) +{ + size_t alen = strlen(another); + return (len == alen) && !memcmp(str, another, alen); +} + +static pgp_armored_msg_t +armor_str_to_data_type(const char *str, size_t len) +{ + if (!str) { + return PGP_ARMORED_UNKNOWN; + } + if (str_equals(str, len, "BEGIN PGP MESSAGE")) { + return PGP_ARMORED_MESSAGE; + } + if (str_equals(str, len, "BEGIN PGP PUBLIC KEY BLOCK") || + str_equals(str, len, "BEGIN PGP PUBLIC KEY")) { + return PGP_ARMORED_PUBLIC_KEY; + } + if (str_equals(str, len, "BEGIN PGP SECRET KEY BLOCK") || + str_equals(str, len, "BEGIN PGP SECRET KEY") || + str_equals(str, len, "BEGIN PGP PRIVATE KEY BLOCK") || + str_equals(str, len, "BEGIN PGP PRIVATE KEY")) { + return PGP_ARMORED_SECRET_KEY; + } + if (str_equals(str, len, "BEGIN PGP SIGNATURE")) { + return PGP_ARMORED_SIGNATURE; + } + if (str_equals(str, len, "BEGIN PGP SIGNED MESSAGE")) { + return PGP_ARMORED_CLEARTEXT; + } + return PGP_ARMORED_UNKNOWN; +} + +pgp_armored_msg_t +rnp_armor_guess_type(pgp_source_t *src) +{ + uint8_t ptag; + + if (!src_peek_eq(src, &ptag, 1)) { + return PGP_ARMORED_UNKNOWN; + } + + switch (get_packet_type(ptag)) { + case PGP_PKT_PK_SESSION_KEY: + case PGP_PKT_SK_SESSION_KEY: + case PGP_PKT_ONE_PASS_SIG: + case PGP_PKT_SE_DATA: + case PGP_PKT_SE_IP_DATA: + case PGP_PKT_COMPRESSED: + case PGP_PKT_LITDATA: + case PGP_PKT_MARKER: + return PGP_ARMORED_MESSAGE; + case PGP_PKT_PUBLIC_KEY: + case PGP_PKT_PUBLIC_SUBKEY: + return PGP_ARMORED_PUBLIC_KEY; + case PGP_PKT_SECRET_KEY: + case PGP_PKT_SECRET_SUBKEY: + return PGP_ARMORED_SECRET_KEY; + case PGP_PKT_SIGNATURE: + return PGP_ARMORED_SIGNATURE; + default: + return PGP_ARMORED_UNKNOWN; + } +} + +static pgp_armored_msg_t +rnp_armored_guess_type_by_readahead(pgp_source_t *src) +{ + if (!src->cache) { + return PGP_ARMORED_UNKNOWN; + } + + pgp_source_t armorsrc = {0}; + pgp_source_t memsrc = {0}; + size_t read; + // peek as much as the cache can take + bool cache_res = src_peek(src, NULL, sizeof(src->cache->buf), &read); + if (!cache_res || !read || + init_mem_src(&memsrc, + src->cache->buf + src->cache->pos, + src->cache->len - src->cache->pos, + false)) { + return PGP_ARMORED_UNKNOWN; + } + rnp_result_t res = init_armored_src(&armorsrc, &memsrc); + if (res) { + src_close(&memsrc); + RNP_LOG("failed to parse armored data"); + return PGP_ARMORED_UNKNOWN; + } + pgp_armored_msg_t guessed = rnp_armor_guess_type(&armorsrc); + src_close(&armorsrc); + src_close(&memsrc); + return guessed; +} + +pgp_armored_msg_t +rnp_armored_get_type(pgp_source_t *src) +{ + pgp_armored_msg_t guessed = rnp_armored_guess_type_by_readahead(src); + if (guessed != PGP_ARMORED_UNKNOWN) { + return guessed; + } + + char hdr[ARMORED_PEEK_BUF_SIZE]; + const char *armhdr; + size_t armhdrlen; + size_t read; + + if (!src_peek(src, hdr, sizeof(hdr), &read) || (read < 20)) { + return PGP_ARMORED_UNKNOWN; + } + if (!(armhdr = find_armor_header(hdr, read, &armhdrlen))) { + return PGP_ARMORED_UNKNOWN; + } + + return armor_str_to_data_type(armhdr + 5, armhdrlen - 10); +} + +static bool +armor_parse_header(pgp_source_t *src) +{ + char hdr[ARMORED_PEEK_BUF_SIZE]; + const char * armhdr; + size_t armhdrlen; + size_t read; + pgp_source_armored_param_t *param = (pgp_source_armored_param_t *) src->param; + + if (!src_peek(param->readsrc, hdr, sizeof(hdr), &read) || (read < 20)) { + return false; + } + + if (!(armhdr = find_armor_header(hdr, read, &armhdrlen))) { + RNP_LOG("no armor header"); + return false; + } + + /* if there are non-whitespaces before the armor header then issue warning */ + for (char *ch = hdr; ch < armhdr; ch++) { + if (B64DEC[(uint8_t) *ch] != 0xfd) { + RNP_LOG("extra data before the header line"); + break; + } + } + + param->type = armor_str_to_data_type(armhdr + 5, armhdrlen - 10); + if (param->type == PGP_ARMORED_UNKNOWN) { + RNP_LOG("unknown armor header"); + return false; + } + + if ((param->armorhdr = (char *) malloc(armhdrlen - 9)) == NULL) { + RNP_LOG("allocation failed"); + return false; + } + + memcpy(param->armorhdr, armhdr + 5, armhdrlen - 10); + param->armorhdr[armhdrlen - 10] = '\0'; + src_skip(param->readsrc, armhdr - hdr + armhdrlen); + armor_skip_chars(param->readsrc, "\t "); + return true; +} + +static bool +armor_skip_line(pgp_source_t *src) +{ + char header[ARMORED_PEEK_BUF_SIZE] = {0}; + do { + size_t hdrlen = 0; + bool res = src_peek_line(src, header, sizeof(header), &hdrlen); + if (hdrlen) { + src_skip(src, hdrlen); + } + if (res || (hdrlen < sizeof(header) - 1)) { + return res; + } + } while (1); +} + +static bool +is_base64_line(const char *line, size_t len) +{ + for (size_t i = 0; i < len && line[i]; i++) { + if (B64DEC[(uint8_t) line[i]] == 0xff) + return false; + } + return true; +} + +static bool +armor_parse_headers(pgp_source_t *src) +{ + pgp_source_armored_param_t *param = (pgp_source_armored_param_t *) src->param; + char header[ARMORED_PEEK_BUF_SIZE] = {0}; + + do { + size_t hdrlen = 0; + if (!src_peek_line(param->readsrc, header, sizeof(header), &hdrlen)) { + /* if line is too long let's cut it to the reasonable size */ + src_skip(param->readsrc, hdrlen); + if ((hdrlen != sizeof(header) - 1) || !armor_skip_line(param->readsrc)) { + RNP_LOG("failed to peek line: unexpected end of data"); + return false; + } + RNP_LOG("Too long armor header - truncated."); + header[hdrlen] = '\0'; + } else if (hdrlen) { + if (is_base64_line(header, hdrlen)) { + RNP_LOG("Warning: no empty line after the base64 headers"); + return true; + } + src_skip(param->readsrc, hdrlen); + if (rnp::is_blank_line(header, hdrlen)) { + return src_skip_eol(param->readsrc); + } + } else { + /* empty line - end of the headers */ + return src_skip_eol(param->readsrc); + } + + char *hdrval = (char *) malloc(hdrlen + 1); + if (!hdrval) { + RNP_LOG("malloc failed"); + return false; + } + + if ((hdrlen >= 9) && !strncmp(header, ST_HEADER_VERSION, 9)) { + memcpy(hdrval, header + 9, hdrlen - 8); + free(param->version); + param->version = hdrval; + } else if ((hdrlen >= 9) && !strncmp(header, ST_HEADER_COMMENT, 9)) { + memcpy(hdrval, header + 9, hdrlen - 8); + free(param->comment); + param->comment = hdrval; + } else if ((hdrlen >= 5) && !strncmp(header, ST_HEADER_HASH, 6)) { + memcpy(hdrval, header + 6, hdrlen - 5); + free(param->hash); + param->hash = hdrval; + } else if ((hdrlen >= 9) && !strncmp(header, ST_HEADER_CHARSET, 9)) { + memcpy(hdrval, header + 9, hdrlen - 8); + free(param->charset); + param->charset = hdrval; + } else { + RNP_LOG("unknown header '%s'", header); + free(hdrval); + } + + if (!src_skip_eol(param->readsrc)) { + return false; + } + } while (1); +} + +rnp_result_t +init_armored_src(pgp_source_t *src, pgp_source_t *readsrc, bool noheaders) +{ + if (!init_src_common(src, 0)) { + return RNP_ERROR_OUT_OF_MEMORY; + } + pgp_source_armored_param_t *param = new (std::nothrow) pgp_source_armored_param_t(); + if (!param) { + RNP_LOG("allocation failed"); + return RNP_ERROR_OUT_OF_MEMORY; + } + + param->readsrc = readsrc; + param->noheaders = noheaders; + src->param = param; + src->read = armored_src_read; + src->close = armored_src_close; + src->type = PGP_STREAM_ARMORED; + + /* base64 data only */ + if (noheaders) { + return RNP_SUCCESS; + } + + /* initialize crc context */ + param->crc_ctx = rnp::CRC24::create(); + /* parsing armored header */ + rnp_result_t errcode = RNP_ERROR_GENERIC; + if (!armor_parse_header(src)) { + errcode = RNP_ERROR_BAD_FORMAT; + goto finish; + } + /* eol */ + if (!src_skip_eol(param->readsrc)) { + RNP_LOG("no eol after the armor header"); + errcode = RNP_ERROR_BAD_FORMAT; + goto finish; + } + /* parsing headers */ + if (!armor_parse_headers(src)) { + RNP_LOG("failed to parse headers"); + errcode = RNP_ERROR_BAD_FORMAT; + goto finish; + } + + /* now we are good to go with base64-encoded data */ + errcode = RNP_SUCCESS; +finish: + if (errcode) { + src_close(src); + } + return errcode; +} + +/** @brief Write message header to the dst. */ +static bool +armor_write_message_header(pgp_dest_armored_param_t *param, bool finish) +{ + const char *str = finish ? ST_ARMOR_END : ST_ARMOR_BEGIN; + dst_write(param->writedst, str, strlen(str)); + switch (param->type) { + case PGP_ARMORED_MESSAGE: + str = "MESSAGE"; + break; + case PGP_ARMORED_PUBLIC_KEY: + str = "PUBLIC KEY BLOCK"; + break; + case PGP_ARMORED_SECRET_KEY: + str = "PRIVATE KEY BLOCK"; + break; + case PGP_ARMORED_SIGNATURE: + str = "SIGNATURE"; + break; + case PGP_ARMORED_CLEARTEXT: + str = "SIGNED MESSAGE"; + break; + default: + return false; + } + dst_write(param->writedst, str, strlen(str)); + dst_write(param->writedst, ST_DASHES, strlen(ST_DASHES)); + return true; +} + +static void +armor_write_eol(pgp_dest_armored_param_t *param) +{ + if (param->eol[0]) { + dst_write(param->writedst, ¶m->eol[0], 1); + } + if (param->eol[1]) { + dst_write(param->writedst, ¶m->eol[1], 1); + } +} + +static void +armor_append_eol(pgp_dest_armored_param_t *param, uint8_t *&ptr) +{ + if (param->eol[0]) { + *ptr++ = param->eol[0]; + } + if (param->eol[1]) { + *ptr++ = param->eol[1]; + } +} + +/* Base 64 encoded table, quadruplicated to save cycles on use & 0x3f operation */ +static const uint8_t B64ENC[256] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', + 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', + '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '+', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', '+', '/'}; + +static void +armored_encode3(uint8_t *out, uint8_t *in) +{ + out[0] = B64ENC[in[0] >> 2]; + out[1] = B64ENC[((in[0] << 4) | (in[1] >> 4)) & 0xff]; + out[2] = B64ENC[((in[1] << 2) | (in[2] >> 6)) & 0xff]; + out[3] = B64ENC[in[2] & 0xff]; +} + +static rnp_result_t +armored_dst_write(pgp_dest_t *dst, const void *buf, size_t len) +{ + pgp_dest_armored_param_t *param = (pgp_dest_armored_param_t *) dst->param; + if (!param) { + RNP_LOG("wrong param"); + return RNP_ERROR_BAD_PARAMETERS; + } + + /* update crc */ + bool base64 = param->type == PGP_ARMORED_BASE64; + if (!base64) { + try { + param->crc_ctx->add(buf, len); + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + return RNP_ERROR_BAD_STATE; + } + } + + uint8_t encbuf[PGP_INPUT_CACHE_SIZE / 2]; + uint8_t *bufptr = (uint8_t *) buf; + uint8_t *bufend = bufptr + len; + uint8_t *encptr = encbuf; + /* processing tail if any */ + if (len + param->tailc < 3) { + memcpy(¶m->tail[param->tailc], buf, len); + param->tailc += len; + return RNP_SUCCESS; + } else if (param->tailc > 0) { + uint8_t dec3[3] = {0}; + memcpy(dec3, param->tail, param->tailc); + memcpy(&dec3[param->tailc], bufptr, 3 - param->tailc); + bufptr += 3 - param->tailc; + param->tailc = 0; + armored_encode3(encptr, dec3); + encptr += 4; + param->lout += 4; + if (param->lout == param->llen) { + armor_append_eol(param, encptr); + param->lout = 0; + } + } + + /* this version prints whole chunks, so rounding down to the closest 4 */ + auto adjusted_llen = param->llen & ~3; + /* number of input bytes to form a whole line of output, param->llen / 4 * 3 */ + auto inllen = (adjusted_llen >> 2) + (adjusted_llen >> 1); + /* pointer to the last full line space in encbuf */ + auto enclast = encbuf + sizeof(encbuf) - adjusted_llen - 2; + + /* processing line chunks, this is the main performance-hitting cycle */ + while (bufptr + 3 <= bufend) { + /* checking whether we have enough space in encbuf */ + if (encptr > enclast) { + dst_write(param->writedst, encbuf, encptr - encbuf); + encptr = encbuf; + } + /* setup length of the input to process in this iteration */ + uint8_t *inlend = + !param->lout ? bufptr + inllen : bufptr + ((adjusted_llen - param->lout) >> 2) * 3; + if (inlend > bufend) { + /* no enough input for the full line */ + inlend = bufptr + (bufend - bufptr) / 3 * 3; + param->lout += (inlend - bufptr) / 3 * 4; + } else { + /* we have full line of input */ + param->lout = 0; + } + + /* processing one line */ + while (bufptr < inlend) { + uint32_t t = (bufptr[0] << 16) | (bufptr[1] << 8) | (bufptr[2]); + bufptr += 3; + *encptr++ = B64ENC[(t >> 18) & 0xff]; + *encptr++ = B64ENC[(t >> 12) & 0xff]; + *encptr++ = B64ENC[(t >> 6) & 0xff]; + *encptr++ = B64ENC[t & 0xff]; + } + + /* adding line ending */ + if (!param->lout) { + armor_append_eol(param, encptr); + } + } + + dst_write(param->writedst, encbuf, encptr - encbuf); + + /* saving tail */ + param->tailc = bufend - bufptr; + memcpy(param->tail, bufptr, param->tailc); + + return RNP_SUCCESS; +} + +static rnp_result_t +armored_dst_finish(pgp_dest_t *dst) +{ + pgp_dest_armored_param_t *param = (pgp_dest_armored_param_t *) dst->param; + + /* writing tail */ + uint8_t buf[5]; + if (param->tailc == 1) { + buf[0] = B64ENC[param->tail[0] >> 2]; + buf[1] = B64ENC[(param->tail[0] << 4) & 0xff]; + buf[2] = CH_EQ; + buf[3] = CH_EQ; + dst_write(param->writedst, buf, 4); + } else if (param->tailc == 2) { + buf[0] = B64ENC[(param->tail[0] >> 2)]; + buf[1] = B64ENC[((param->tail[0] << 4) | (param->tail[1] >> 4)) & 0xff]; + buf[2] = B64ENC[(param->tail[1] << 2) & 0xff]; + buf[3] = CH_EQ; + dst_write(param->writedst, buf, 4); + } + /* Check for base64 */ + if (param->type == PGP_ARMORED_BASE64) { + return param->writedst->werr; + } + + /* writing EOL if needed */ + if ((param->tailc > 0) || (param->lout > 0)) { + armor_write_eol(param); + } + + /* writing CRC and EOL */ + // At this point crc_ctx is initialized, so call can't fail + buf[0] = CH_EQ; + try { + auto crc = param->crc_ctx->finish(); + armored_encode3(&buf[1], crc.data()); + } catch (const std::exception &e) { + RNP_LOG("%s", e.what()); + } + dst_write(param->writedst, buf, 5); + armor_write_eol(param); + + /* writing armor header */ + if (!armor_write_message_header(param, true)) { + return RNP_ERROR_BAD_PARAMETERS; + } + armor_write_eol(param); + return param->writedst->werr; +} + +static void +armored_dst_close(pgp_dest_t *dst, bool discard) +{ + pgp_dest_armored_param_t *param = (pgp_dest_armored_param_t *) dst->param; + + if (!param) { + return; + } + /* dst_close may be called without dst_finish on error */ + delete param; + dst->param = NULL; +} + +rnp_result_t +init_armored_dst(pgp_dest_t *dst, pgp_dest_t *writedst, pgp_armored_msg_t msgtype) +{ + if (!init_dst_common(dst, 0)) { + return RNP_ERROR_OUT_OF_MEMORY; + } + pgp_dest_armored_param_t *param = new (std::nothrow) pgp_dest_armored_param_t(); + if (!param) { + return RNP_ERROR_OUT_OF_MEMORY; + } + + dst->param = param; + dst->write = armored_dst_write; + dst->finish = armored_dst_finish; + dst->close = armored_dst_close; + dst->type = PGP_STREAM_ARMORED; + dst->writeb = 0; + dst->clen = 0; + + param->writedst = writedst; + param->type = msgtype; + /* Base64 message */ + if (msgtype == PGP_ARMORED_BASE64) { + /* Base64 encoding will not output EOLs but we need this to not duplicate code for a + * separate base64_dst_write function */ + param->eol[0] = 0; + param->eol[1] = 0; + param->llen = 256; + return RNP_SUCCESS; + } + /* create crc context */ + param->crc_ctx = rnp::CRC24::create(); + param->eol[0] = CH_CR; + param->eol[1] = CH_LF; + param->llen = 76; /* must be multiple of 4 */ + /* armor header */ + if (!armor_write_message_header(param, false)) { + RNP_LOG("unknown data type"); + armored_dst_close(dst, true); + return RNP_ERROR_BAD_PARAMETERS; + } + armor_write_eol(param); + /* empty line */ + armor_write_eol(param); + return RNP_SUCCESS; +} + +bool +is_armored_dest(pgp_dest_t *dst) +{ + return dst->type == PGP_STREAM_ARMORED; +} + +rnp_result_t +armored_dst_set_line_length(pgp_dest_t *dst, size_t llen) +{ + if (!dst || (llen < ARMORED_MIN_LINE_LENGTH) || (llen > ARMORED_MAX_LINE_LENGTH) || + !dst->param || !is_armored_dest(dst)) { + return RNP_ERROR_BAD_PARAMETERS; + } + auto param = (pgp_dest_armored_param_t *) dst->param; + param->llen = llen; + return RNP_SUCCESS; +} + +bool +is_armored_source(pgp_source_t *src) +{ + uint8_t buf[ARMORED_PEEK_BUF_SIZE]; + size_t read = 0; + + if (!src_peek(src, buf, sizeof(buf), &read) || (read < strlen(ST_ARMOR_BEGIN) + 1)) { + return false; + } + buf[read - 1] = 0; + return !!strstr((char *) buf, ST_ARMOR_BEGIN); +} + +bool +is_cleartext_source(pgp_source_t *src) +{ + uint8_t buf[ARMORED_PEEK_BUF_SIZE]; + size_t read = 0; + + if (!src_peek(src, buf, sizeof(buf), &read) || (read < strlen(ST_CLEAR_BEGIN))) { + return false; + } + buf[read - 1] = 0; + return !!strstr((char *) buf, ST_CLEAR_BEGIN); +} + +bool +is_base64_source(pgp_source_t &src) +{ + char buf[128]; + size_t read = 0; + + if (!src_peek(&src, buf, sizeof(buf), &read) || (read < 4)) { + return false; + } + return is_base64_line(buf, read); +} + +rnp_result_t +rnp_dearmor_source(pgp_source_t *src, pgp_dest_t *dst) +{ + rnp_result_t res = RNP_ERROR_BAD_FORMAT; + pgp_source_t armorsrc = {0}; + + /* initializing armored message */ + res = init_armored_src(&armorsrc, src); + if (res) { + return res; + } + /* Reading data from armored source and writing it to the output */ + res = dst_write_src(&armorsrc, dst); + if (res) { + RNP_LOG("dearmoring failed"); + } + + src_close(&armorsrc); + return res; +} + +rnp_result_t +rnp_armor_source(pgp_source_t *src, pgp_dest_t *dst, pgp_armored_msg_t msgtype) +{ + pgp_dest_t armordst = {0}; + rnp_result_t res = init_armored_dst(&armordst, dst, msgtype); + if (res) { + return res; + } + + res = dst_write_src(src, &armordst); + if (res) { + RNP_LOG("armoring failed"); + } + + dst_close(&armordst, res != RNP_SUCCESS); + return res; +} + +namespace rnp { + +const uint32_t ArmoredSource::AllowBinary = 0x01; +const uint32_t ArmoredSource::AllowBase64 = 0x02; +const uint32_t ArmoredSource::AllowMultiple = 0x04; + +ArmoredSource::ArmoredSource(pgp_source_t &readsrc, uint32_t flags) + : Source(), readsrc_(readsrc), multiple_(false) +{ + /* Do not dearmor already armored stream */ + bool already = readsrc_.type == PGP_STREAM_ARMORED; + /* Check for base64 source: no multiple streams allowed */ + if (!already && (flags & AllowBase64) && (is_base64_source(readsrc))) { + auto res = init_armored_src(&src_, &readsrc_, true); + if (res) { + RNP_LOG("Failed to parse base64 data."); + throw rnp::rnp_exception(res); + } + armored_ = true; + return; + } + /* Check for armored source */ + if (!already && is_armored_source(&readsrc)) { + auto res = init_armored_src(&src_, &readsrc_); + if (res) { + RNP_LOG("Failed to parse armored data."); + throw rnp::rnp_exception(res); + } + armored_ = true; + multiple_ = flags & AllowMultiple; + return; + } + /* Use binary source if allowed */ + if (!(flags & AllowBinary)) { + RNP_LOG("Non-armored data is not allowed here."); + throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS); + } + armored_ = false; +} + +void +ArmoredSource::restart() +{ + if (!armored_ || src_eof(&readsrc_) || src_error(&readsrc_)) { + return; + } + src_close(&src_); + auto res = init_armored_src(&src_, &readsrc_); + if (res) { + throw rnp::rnp_exception(res); + } +} + +pgp_source_t & +ArmoredSource::src() +{ + return armored_ ? src_ : readsrc_; +} +} // namespace rnp |