From 32984d8fc3dbb90a3fafb69fece0134f1ea790f9 Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Wed, 1 Oct 2014 15:56:26 +0300 Subject: [PATCH] Add functions for dealing with PGP armor header lines to pgcrypto. This add a new pgp_armor_headers function to extract armor headers from an ASCII-armored blob, and a new overloaded variant of the armor function, for constructing an ASCII-armor with extra headers. Marko Tiikkaja and me. --- .gitattributes | 1 + contrib/pgcrypto/Makefile | 3 +- contrib/pgcrypto/expected/pgp-armor.out | 268 ++++++++++++++++++ contrib/pgcrypto/pgcrypto--1.1--1.2.sql | 14 + .../{pgcrypto--1.1.sql => pgcrypto--1.2.sql} | 10 + contrib/pgcrypto/pgcrypto.control | 2 +- contrib/pgcrypto/pgp-armor.c | 119 +++++++- contrib/pgcrypto/pgp-pgsql.c | 203 ++++++++++++- contrib/pgcrypto/pgp.h | 5 +- contrib/pgcrypto/sql/pgp-armor.sql | 158 +++++++++++ doc/src/sgml/pgcrypto.sgml | 28 +- 11 files changed, 804 insertions(+), 7 deletions(-) create mode 100644 contrib/pgcrypto/pgcrypto--1.1--1.2.sql rename contrib/pgcrypto/{pgcrypto--1.1.sql => pgcrypto--1.2.sql} (94%) diff --git a/.gitattributes b/.gitattributes index ff96567ca5..9466800e12 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,6 +14,7 @@ README.* conflict-marker-size=32 # Certain data files that contain special whitespace, and other special cases *.data -whitespace contrib/tsearch2/sql/tsearch2.sql whitespace=space-before-tab,blank-at-eof,-blank-at-eol +contrib/pgcrypto/sql/pgp-armor.sql whitespace=-blank-at-eol doc/bug.template whitespace=space-before-tab,-blank-at-eof,blank-at-eol src/backend/catalog/sql_features.txt whitespace=space-before-tab,blank-at-eof,-blank-at-eol src/backend/tsearch/hunspell_sample.affix whitespace=-blank-at-eof diff --git a/contrib/pgcrypto/Makefile b/contrib/pgcrypto/Makefile index b6c94844b2..18bad1a05f 100644 --- a/contrib/pgcrypto/Makefile +++ b/contrib/pgcrypto/Makefile @@ -26,7 +26,8 @@ MODULE_big = pgcrypto OBJS = $(SRCS:.c=.o) $(WIN32RES) EXTENSION = pgcrypto -DATA = pgcrypto--1.1.sql pgcrypto--1.0--1.1.sql pgcrypto--unpackaged--1.0.sql +DATA = pgcrypto--1.2.sql pgcrypto--1.1--1.2.sql pgcrypto--1.0--1.1.sql \ + pgcrypto--unpackaged--1.0.sql PGFILEDESC = "pgcrypto - cryptographic functions" REGRESS = init md5 sha1 hmac-md5 hmac-sha1 blowfish rijndael \ diff --git a/contrib/pgcrypto/expected/pgp-armor.out b/contrib/pgcrypto/expected/pgp-armor.out index c95549412e..89d410a7dc 100644 --- a/contrib/pgcrypto/expected/pgp-armor.out +++ b/contrib/pgcrypto/expected/pgp-armor.out @@ -102,3 +102,271 @@ em9va2E= -----END PGP MESSAGE----- '); ERROR: Corrupt ascii-armor +-- corrupt (no space after the colon) +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +foo: + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); +ERROR: Corrupt ascii-armor +-- corrupt (no empty line) +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); +ERROR: Corrupt ascii-armor +-- no headers +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + key | value +-----+------- +(0 rows) + +-- header with empty value +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +foo: + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + key | value +-----+------- + foo | +(1 row) + +-- simple +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +fookey: foovalue +barkey: barvalue + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + key | value +--------+---------- + fookey | foovalue + barkey | barvalue +(2 rows) + +-- insane keys, part 1 +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +insane:key : + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + key | value +-------------+------- + insane:key | +(1 row) + +-- insane keys, part 2 +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +insane:key : text value here + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + key | value +-------------+----------------- + insane:key | text value here +(1 row) + +-- long value +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +long: this value is more than 76 characters long, but it should still parse correctly as that''s permitted by RFC 4880 + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + key | value +------+----------------------------------------------------------------------------------------------------------------- + long | this value is more than 76 characters long, but it should still parse correctly as that's permitted by RFC 4880 +(1 row) + +-- long value, split up +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +long: this value is more than 76 characters long, but it should still +long: parse correctly as that''s permitted by RFC 4880 + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + key | value +------+------------------------------------------------------------------ + long | this value is more than 76 characters long, but it should still + long | parse correctly as that's permitted by RFC 4880 +(2 rows) + +-- long value, split up, part 2 +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +long: this value is more than +long: 76 characters long, but it should still +long: parse correctly as that''s permitted by RFC 4880 + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + key | value +------+------------------------------------------------- + long | this value is more than + long | 76 characters long, but it should still + long | parse correctly as that's permitted by RFC 4880 +(3 rows) + +-- long value, split up, part 3 +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +emptykey: +long: this value is more than +emptykey: +long: 76 characters long, but it should still +emptykey: +long: parse correctly as that''s permitted by RFC 4880 +emptykey: + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + key | value +----------+------------------------------------------------- + emptykey | + long | this value is more than + emptykey | + long | 76 characters long, but it should still + emptykey | + long | parse correctly as that's permitted by RFC 4880 + emptykey | +(7 rows) + +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +Comment: dat1.blowfish.sha1.mdc.s2k3.z0 + +jA0EBAMCfFNwxnvodX9g0jwB4n4s26/g5VmKzVab1bX1SmwY7gvgvlWdF3jKisvS +yA6Ce1QTMK3KdL2MPfamsTUSAML8huCJMwYQFfE= +=JcP+ +-----END PGP MESSAGE----- +'); + key | value +---------+-------------------------------- + Comment | dat1.blowfish.sha1.mdc.s2k3.z0 +(1 row) + +-- test CR+LF line endings +select * from pgp_armor_headers(replace(' +-----BEGIN PGP MESSAGE----- +fookey: foovalue +barkey: barvalue + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +', E'\n', E'\r\n')); + key | value +--------+---------- + fookey | foovalue + barkey | barvalue +(2 rows) + +-- test header generation +select armor('zooka', array['foo'], array['bar']); + armor +----------------------------- + -----BEGIN PGP MESSAGE-----+ + foo: bar + + + + em9va2E= + + =D5cR + + -----END PGP MESSAGE----- + + +(1 row) + +select armor('zooka', array['Version', 'Comment'], array['Created by pgcrypto', 'PostgreSQL, the world''s most advanced open source database']); + armor +--------------------------------------------------------------------- + -----BEGIN PGP MESSAGE----- + + Version: Created by pgcrypto + + Comment: PostgreSQL, the world's most advanced open source database+ + + + em9va2E= + + =D5cR + + -----END PGP MESSAGE----- + + +(1 row) + +select * from pgp_armor_headers( + armor('zooka', array['Version', 'Comment'], + array['Created by pgcrypto', 'PostgreSQL, the world''s most advanced open source database'])); + key | value +---------+------------------------------------------------------------ + Version | Created by pgcrypto + Comment | PostgreSQL, the world's most advanced open source database +(2 rows) + +-- error/corner cases +select armor('', array['foo'], array['too', 'many']); +ERROR: mismatched array dimensions +select armor('', array['too', 'many'], array['foo']); +ERROR: mismatched array dimensions +select armor('', array[['']], array['foo']); +ERROR: wrong number of array subscripts +select armor('', array['foo'], array[['']]); +ERROR: wrong number of array subscripts +select armor('', array[null], array['foo']); +ERROR: null value not allowed for header key +select armor('', array['foo'], array[null]); +ERROR: null value not allowed for header value +select armor('', '[0:0]={"foo"}', array['foo']); + armor +----------------------------- + -----BEGIN PGP MESSAGE-----+ + foo: foo + + + + =twTO + + -----END PGP MESSAGE----- + + +(1 row) + +select armor('', array['foo'], '[0:0]={"foo"}'); + armor +----------------------------- + -----BEGIN PGP MESSAGE-----+ + foo: foo + + + + =twTO + + -----END PGP MESSAGE----- + + +(1 row) + +select armor('', array[E'embedded\nnewline'], array['foo']); +ERROR: header key must not contain newlines +select armor('', array['foo'], array[E'embedded\nnewline']); +ERROR: header value must not contain newlines +select armor('', array['embedded: colon+space'], array['foo']); +ERROR: header key must not contain ": " diff --git a/contrib/pgcrypto/pgcrypto--1.1--1.2.sql b/contrib/pgcrypto/pgcrypto--1.1--1.2.sql new file mode 100644 index 0000000000..753e169384 --- /dev/null +++ b/contrib/pgcrypto/pgcrypto--1.1--1.2.sql @@ -0,0 +1,14 @@ +/* contrib/pgcrypto/pgcrypto--1.1--1.2.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION pgcrypto UPDATE TO '1.2'" to load this file. \quit + +CREATE FUNCTION armor(bytea, text[], text[]) +RETURNS text +AS 'MODULE_PATHNAME', 'pg_armor' +LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION pgp_armor_headers(text, key OUT text, value OUT text) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pgp_armor_headers' +LANGUAGE C IMMUTABLE STRICT; diff --git a/contrib/pgcrypto/pgcrypto--1.1.sql b/contrib/pgcrypto/pgcrypto--1.2.sql similarity index 94% rename from contrib/pgcrypto/pgcrypto--1.1.sql rename to contrib/pgcrypto/pgcrypto--1.2.sql index a260857d30..370a9a19cd 100644 --- a/contrib/pgcrypto/pgcrypto--1.1.sql +++ b/contrib/pgcrypto/pgcrypto--1.2.sql @@ -201,7 +201,17 @@ RETURNS text AS 'MODULE_PATHNAME', 'pg_armor' LANGUAGE C IMMUTABLE STRICT; +CREATE FUNCTION armor(bytea, text[], text[]) +RETURNS text +AS 'MODULE_PATHNAME', 'pg_armor' +LANGUAGE C IMMUTABLE STRICT; + CREATE FUNCTION dearmor(text) RETURNS bytea AS 'MODULE_PATHNAME', 'pg_dearmor' LANGUAGE C IMMUTABLE STRICT; + +CREATE FUNCTION pgp_armor_headers(text, key OUT text, value OUT text) +RETURNS SETOF record +AS 'MODULE_PATHNAME', 'pgp_armor_headers' +LANGUAGE C IMMUTABLE STRICT; diff --git a/contrib/pgcrypto/pgcrypto.control b/contrib/pgcrypto/pgcrypto.control index 7f79d044ab..bb6885bc1b 100644 --- a/contrib/pgcrypto/pgcrypto.control +++ b/contrib/pgcrypto/pgcrypto.control @@ -1,5 +1,5 @@ # pgcrypto extension comment = 'cryptographic functions' -default_version = '1.1' +default_version = '1.2' module_pathname = '$libdir/pgcrypto' relocatable = true diff --git a/contrib/pgcrypto/pgp-armor.c b/contrib/pgcrypto/pgp-armor.c index ec647f0f3f..24eb42fa89 100644 --- a/contrib/pgcrypto/pgp-armor.c +++ b/contrib/pgcrypto/pgp-armor.c @@ -178,7 +178,7 @@ b64_dec_len(unsigned srclen) * PGP armor */ -static const char *armor_header = "-----BEGIN PGP MESSAGE-----\n\n"; +static const char *armor_header = "-----BEGIN PGP MESSAGE-----\n"; static const char *armor_footer = "\n-----END PGP MESSAGE-----\n"; /* CRC24 implementation from rfc2440 */ @@ -204,17 +204,24 @@ crc24(const uint8 *data, unsigned len) } void -pgp_armor_encode(const uint8 *src, int len, StringInfo dst) +pgp_armor_encode(const uint8 *src, unsigned len, StringInfo dst, + int num_headers, char **keys, char **values) { + int n; int res; unsigned b64len; unsigned crc = crc24(src, len); appendStringInfoString(dst, armor_header); + for (n = 0; n < num_headers; n++) + appendStringInfo(dst, "%s: %s\n", keys[n], values[n]); + appendStringInfoChar(dst, '\n'); + /* make sure we have enough room to b64_encode() */ b64len = b64_enc_len(len); enlargeStringInfo(dst, (int) b64len); + res = b64_encode(src, len, (uint8 *) dst->data + dst->len); if (res > b64len) elog(FATAL, "overflow - encode estimate too small"); @@ -371,3 +378,111 @@ pgp_armor_decode(const uint8 *src, int len, StringInfo dst) out: return res; } + +/* + * Extracts all armor headers from an ASCII-armored input. + * + * Returns 0 on success, or PXE_* error code on error. On success, the + * number of headers and their keys and values are returned in *nheaders, + * *nkeys and *nvalues. + */ +int +pgp_extract_armor_headers(const uint8 *src, unsigned len, + int *nheaders, char ***keys, char ***values) +{ + const uint8 *data_end = src + len; + const uint8 *p; + const uint8 *base64_start; + const uint8 *armor_start; + const uint8 *armor_end; + Size armor_len; + char *line; + char *nextline; + char *eol, + *colon; + int hlen; + char *buf; + int hdrlines; + int n; + + /* armor start */ + hlen = find_header(src, data_end, &armor_start, 0); + if (hlen <= 0) + return PXE_PGP_CORRUPT_ARMOR; + armor_start += hlen; + + /* armor end */ + hlen = find_header(armor_start, data_end, &armor_end, 1); + if (hlen <= 0) + return PXE_PGP_CORRUPT_ARMOR; + + /* Count the number of armor header lines. */ + hdrlines = 0; + p = armor_start; + while (p < armor_end && *p != '\n' && *p != '\r') + { + p = memchr(p, '\n', armor_end - p); + if (!p) + return PXE_PGP_CORRUPT_ARMOR; + + /* step to start of next line */ + p++; + hdrlines++; + } + base64_start = p; + + /* + * Make a modifiable copy of the part of the input that contains the + * headers. The returned key/value pointers will point inside the buffer. + */ + armor_len = base64_start - armor_start; + buf = palloc(armor_len + 1); + memcpy(buf, armor_start, armor_len); + buf[armor_len] = '\0'; + + /* Allocate return arrays */ + *keys = (char **) palloc(hdrlines * sizeof(char *)); + *values = (char **) palloc(hdrlines * sizeof(char *)); + + /* + * Split the header lines at newlines and ": " separators, and collect + * pointers to the keys and values in the return arrays. + */ + n = 0; + line = buf; + for (;;) + { + /* find end of line */ + eol = strchr(line, '\n'); + if (!eol) + break; + nextline = eol + 1; + /* if the line ends in CR + LF, strip the CR */ + if (eol > line && *(eol - 1) == '\r') + eol--; + *eol = '\0'; + + /* find colon+space separating the key and value */ + colon = strstr(line, ": "); + if (!colon) + return PXE_PGP_CORRUPT_ARMOR; + *colon = '\0'; + + /* shouldn't happen, we counted the number of lines beforehand */ + if (n >= hdrlines) + elog(ERROR, "unexpected number of armor header lines"); + + (*keys)[n] = line; + (*values)[n] = colon + 2; + n++; + + /* step to start of next line */ + line = nextline; + } + + if (n != hdrlines) + elog(ERROR, "unexpected number of armor header lines"); + + *nheaders = n; + return 0; +} diff --git a/contrib/pgcrypto/pgp-pgsql.c b/contrib/pgcrypto/pgp-pgsql.c index 5d2d4655d1..2070873140 100644 --- a/contrib/pgcrypto/pgp-pgsql.c +++ b/contrib/pgcrypto/pgp-pgsql.c @@ -32,8 +32,11 @@ #include "postgres.h" #include "lib/stringinfo.h" +#include "catalog/pg_type.h" #include "mb/pg_wchar.h" #include "utils/builtins.h" +#include "utils/array.h" +#include "funcapi.h" #include "mbuf.h" #include "px.h" @@ -56,6 +59,7 @@ PG_FUNCTION_INFO_V1(pgp_key_id_w); PG_FUNCTION_INFO_V1(pg_armor); PG_FUNCTION_INFO_V1(pg_dearmor); +PG_FUNCTION_INFO_V1(pgp_armor_headers); /* * Mix a block of data into RNG. @@ -148,6 +152,19 @@ convert_to_utf8(text *src) return convert_charset(src, GetDatabaseEncoding(), PG_UTF8); } +static bool +string_is_ascii(const char *str) +{ + const char *p; + + for (p = str; *p; p++) + { + if (IS_HIGHBIT_SET(*p)) + return false; + } + return true; +} + static void clear_and_pfree(text *p) { @@ -816,6 +833,100 @@ pgp_pub_decrypt_text(PG_FUNCTION_ARGS) * Wrappers for PGP ascii armor */ +/* + * Helper function for pgp_armor. Converts arrays of keys and values into + * plain C arrays, and checks that they don't contain invalid characters. + */ +static int +parse_key_value_arrays(ArrayType *key_array, ArrayType *val_array, + char ***p_keys, char ***p_values) +{ + int nkdims = ARR_NDIM(key_array); + int nvdims = ARR_NDIM(val_array); + char **keys, + **values; + Datum *key_datums, + *val_datums; + bool *key_nulls, + *val_nulls; + int key_count, + val_count; + int i; + + if (nkdims > 1 || nkdims != nvdims) + ereport(ERROR, + (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), + errmsg("wrong number of array subscripts"))); + if (nkdims == 0) + return 0; + + deconstruct_array(key_array, + TEXTOID, -1, false, 'i', + &key_datums, &key_nulls, &key_count); + + deconstruct_array(val_array, + TEXTOID, -1, false, 'i', + &val_datums, &val_nulls, &val_count); + + if (key_count != val_count) + ereport(ERROR, + (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), + errmsg("mismatched array dimensions"))); + + keys = (char **) palloc(sizeof(char *) * key_count); + values = (char **) palloc(sizeof(char *) * val_count); + + for (i = 0; i < key_count; i++) + { + char *v; + + /* Check that the key doesn't contain anything funny */ + if (key_nulls[i]) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value not allowed for header key"))); + + v = TextDatumGetCString(key_datums[i]); + + if (!string_is_ascii(v)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("header key must not contain non-ASCII characters"))); + if (strstr(v, ": ")) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("header key must not contain \": \""))); + if (strchr(v, '\n')) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("header key must not contain newlines"))); + keys[i] = v; + + /* And the same for the value */ + if (val_nulls[i]) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("null value not allowed for header value"))); + + v = TextDatumGetCString(val_datums[i]); + + if (!string_is_ascii(v)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("header value must not contain non-ASCII characters"))); + if (strchr(v, '\n')) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("header value must not contain newlines"))); + + values[i] = v; + } + + *p_keys = keys; + *p_values = values; + return key_count; +} + Datum pg_armor(PG_FUNCTION_ARGS) { @@ -823,13 +934,27 @@ pg_armor(PG_FUNCTION_ARGS) text *res; int data_len; StringInfoData buf; + int num_headers; + char **keys = NULL, + **values = NULL; data = PG_GETARG_BYTEA_P(0); data_len = VARSIZE(data) - VARHDRSZ; + if (PG_NARGS() == 3) + { + num_headers = parse_key_value_arrays(PG_GETARG_ARRAYTYPE_P(1), + PG_GETARG_ARRAYTYPE_P(2), + &keys, &values); + } + else if (PG_NARGS() == 1) + num_headers = 0; + else + elog(ERROR, "unexpected number of arguments %d", PG_NARGS()); initStringInfo(&buf); - pgp_armor_encode((uint8 *) VARDATA(data), data_len, &buf); + pgp_armor_encode((uint8 *) VARDATA(data), data_len, &buf, + num_headers, keys, values); res = palloc(VARHDRSZ + buf.len); SET_VARSIZE(res, VARHDRSZ + buf.len); @@ -868,6 +993,82 @@ pg_dearmor(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(res); } +/* cross-call state for pgp_armor_headers */ +typedef struct +{ + int nheaders; + char **keys; + char **values; +} pgp_armor_headers_state; + +Datum +pgp_armor_headers(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + pgp_armor_headers_state *state; + char *utf8key; + char *utf8val; + HeapTuple tuple; + TupleDesc tupdesc; + AttInMetadata *attinmeta; + + if (SRF_IS_FIRSTCALL()) + { + text *data = PG_GETARG_TEXT_PP(0); + int res; + MemoryContext oldcontext; + + funcctx = SRF_FIRSTCALL_INIT(); + + /* we need the state allocated in the multi call context */ + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + /* Build a tuple descriptor for our result type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + attinmeta = TupleDescGetAttInMetadata(tupdesc); + funcctx->attinmeta = attinmeta; + + state = (pgp_armor_headers_state *) palloc(sizeof(pgp_armor_headers_state)); + + res = pgp_extract_armor_headers((uint8 *) VARDATA_ANY(data), + VARSIZE_ANY_EXHDR(data), + &state->nheaders, &state->keys, + &state->values); + if (res < 0) + ereport(ERROR, + (errcode(ERRCODE_EXTERNAL_ROUTINE_INVOCATION_EXCEPTION), + errmsg("%s", px_strerror(res)))); + + MemoryContextSwitchTo(oldcontext); + funcctx->user_fctx = state; + } + + funcctx = SRF_PERCALL_SETUP(); + state = (pgp_armor_headers_state *) funcctx->user_fctx; + + if (funcctx->call_cntr >= state->nheaders) + SRF_RETURN_DONE(funcctx); + else + { + char *values[2]; + + /* we assume that the keys (and values) are in UTF-8. */ + utf8key = state->keys[funcctx->call_cntr]; + utf8val = state->values[funcctx->call_cntr]; + + values[0] = pg_any_to_server(utf8key, strlen(utf8key), PG_UTF8); + values[1] = pg_any_to_server(utf8val, strlen(utf8val), PG_UTF8); + + /* build a tuple */ + tuple = BuildTupleFromCStrings(funcctx->attinmeta, values); + SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple)); + } +} + + + /* * Wrappers for PGP key id */ diff --git a/contrib/pgcrypto/pgp.h b/contrib/pgcrypto/pgp.h index cecd181495..398f21bca2 100644 --- a/contrib/pgcrypto/pgp.h +++ b/contrib/pgcrypto/pgp.h @@ -276,8 +276,11 @@ void pgp_cfb_free(PGP_CFB *ctx); int pgp_cfb_encrypt(PGP_CFB *ctx, const uint8 *data, int len, uint8 *dst); int pgp_cfb_decrypt(PGP_CFB *ctx, const uint8 *data, int len, uint8 *dst); -void pgp_armor_encode(const uint8 *src, int len, StringInfo dst); +void pgp_armor_encode(const uint8 *src, unsigned len, StringInfo dst, + int num_headers, char **keys, char **values); int pgp_armor_decode(const uint8 *src, int len, StringInfo dst); +int pgp_extract_armor_headers(const uint8 *src, unsigned len, + int *nheaders, char ***keys, char ***values); int pgp_compress_filter(PushFilter **res, PGP_Context *ctx, PushFilter *dst); int pgp_decompress_filter(PullFilter **res, PGP_Context *ctx, PullFilter *src); diff --git a/contrib/pgcrypto/sql/pgp-armor.sql b/contrib/pgcrypto/sql/pgp-armor.sql index 71ffba26a0..a277a1894c 100644 --- a/contrib/pgcrypto/sql/pgp-armor.sql +++ b/contrib/pgcrypto/sql/pgp-armor.sql @@ -56,3 +56,161 @@ em9va2E= =ZZZZ -----END PGP MESSAGE----- '); + +-- corrupt (no space after the colon) +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +foo: + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + +-- corrupt (no empty line) +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + +-- no headers +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + +-- header with empty value +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +foo: + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + +-- simple +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +fookey: foovalue +barkey: barvalue + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + +-- insane keys, part 1 +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +insane:key : + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + +-- insane keys, part 2 +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +insane:key : text value here + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + +-- long value +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +long: this value is more than 76 characters long, but it should still parse correctly as that''s permitted by RFC 4880 + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + +-- long value, split up +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +long: this value is more than 76 characters long, but it should still +long: parse correctly as that''s permitted by RFC 4880 + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + +-- long value, split up, part 2 +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +long: this value is more than +long: 76 characters long, but it should still +long: parse correctly as that''s permitted by RFC 4880 + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + +-- long value, split up, part 3 +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +emptykey: +long: this value is more than +emptykey: +long: 76 characters long, but it should still +emptykey: +long: parse correctly as that''s permitted by RFC 4880 +emptykey: + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +'); + +select * from pgp_armor_headers(' +-----BEGIN PGP MESSAGE----- +Comment: dat1.blowfish.sha1.mdc.s2k3.z0 + +jA0EBAMCfFNwxnvodX9g0jwB4n4s26/g5VmKzVab1bX1SmwY7gvgvlWdF3jKisvS +yA6Ce1QTMK3KdL2MPfamsTUSAML8huCJMwYQFfE= +=JcP+ +-----END PGP MESSAGE----- +'); + +-- test CR+LF line endings +select * from pgp_armor_headers(replace(' +-----BEGIN PGP MESSAGE----- +fookey: foovalue +barkey: barvalue + +em9va2E= +=ZZZZ +-----END PGP MESSAGE----- +', E'\n', E'\r\n')); + +-- test header generation +select armor('zooka', array['foo'], array['bar']); +select armor('zooka', array['Version', 'Comment'], array['Created by pgcrypto', 'PostgreSQL, the world''s most advanced open source database']); +select * from pgp_armor_headers( + armor('zooka', array['Version', 'Comment'], + array['Created by pgcrypto', 'PostgreSQL, the world''s most advanced open source database'])); + +-- error/corner cases +select armor('', array['foo'], array['too', 'many']); +select armor('', array['too', 'many'], array['foo']); +select armor('', array[['']], array['foo']); +select armor('', array['foo'], array[['']]); +select armor('', array[null], array['foo']); +select armor('', array['foo'], array[null]); +select armor('', '[0:0]={"foo"}', array['foo']); +select armor('', array['foo'], '[0:0]={"foo"}'); +select armor('', array[E'embedded\nnewline'], array['foo']); +select armor('', array['foo'], array[E'embedded\nnewline']); +select armor('', array['embedded: colon+space'], array['foo']); diff --git a/doc/src/sgml/pgcrypto.sgml b/doc/src/sgml/pgcrypto.sgml index 544a1f8346..f0928f80fe 100644 --- a/doc/src/sgml/pgcrypto.sgml +++ b/doc/src/sgml/pgcrypto.sgml @@ -691,13 +691,39 @@ pgp_key_id(bytea) returns text -armor(data bytea) returns text +armor(data bytea [ , keys text[], values text[] ]) returns text dearmor(data text) returns bytea These functions wrap/unwrap binary data into PGP ASCII-armor format, which is basically Base64 with CRC and additional formatting. + + + If the keys and values arrays are specified, + an armor header is added to the armored format for each + key/value pair. Both arrays must be single-dimensional, and they must + be of the same length. The keys and values cannot contain any non-ASCII + characters. + + + + + <function>pgp_armor_headers</function> + + + pgp_armor_headers + + + +pgp_armor_headers(data text, key out text, value out text) returns setof record + + + pgp_armor_headers() extracts the armor headers from + data. The return value is a set of rows with two columns, + key and value. If the keys or values contain any non-ASCII characters, + they are treated as UTF-8. + -- 2.40.0