On 10/01/2014 11:58 AM, Marko Tiikkaja wrote:
On 10/1/14, 9:11 AM, Heikki Linnakangas wrote:
We have two options:
1. Throw an error if there are any non-ASCII characters in the key/value
arrays.
2. Don't convert them to UTF-8, but use the current database encoding.
Both seem sane to me. If we use the current database encoding, then we
have to also decide what to do with the input, in pgp_armor_headers().
If armor() uses the database encoding, but pgp_armor_headers() treats
the input as UTF-8, then a round-trip with pgp_armor_headers(armor(?))
won't work.
Yeah. Both options seem fine to me. Throwing an error perhaps slightly
more so.
I went with 1, throw an error. I also added checks that the key or value
doesn't contain any embedded newlines, and that the key doesn't contain
an embedded ": ". Those would cause the armor to be invalid.
I think this is now ready for commit, but since I've changed it quite
significantly from what you originally submitted, please take a moment
to review this.
- Heikki
diff --git a/.gitattributes b/.gitattributes
index ff96567..9466800 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 b6c9484..d1d75ca 100644
--- a/contrib/pgcrypto/Makefile
+++ b/contrib/pgcrypto/Makefile
@@ -26,7 +26,7 @@ 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 c955494..a8f73ac 100644
--- a/contrib/pgcrypto/expected/pgp-armor.out
+++ b/contrib/pgcrypto/expected/pgp-armor.out
@@ -102,3 +102,251 @@ 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
+-- 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 0000000..753e169
--- /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.2.sql b/contrib/pgcrypto/pgcrypto--1.2.sql
new file mode 100644
index 0000000..370a9a1
--- /dev/null
+++ b/contrib/pgcrypto/pgcrypto--1.2.sql
@@ -0,0 +1,217 @@
+/* contrib/pgcrypto/pgcrypto--1.1.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pgcrypto" to load this file. \quit
+
+CREATE FUNCTION digest(text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_digest'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION digest(bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_digest'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION hmac(text, text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_hmac'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION hmac(bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_hmac'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION crypt(text, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pg_crypt'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION gen_salt(text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pg_gen_salt'
+LANGUAGE C VOLATILE STRICT;
+
+CREATE FUNCTION gen_salt(text, int4)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pg_gen_salt_rounds'
+LANGUAGE C VOLATILE STRICT;
+
+CREATE FUNCTION encrypt(bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_encrypt'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION decrypt(bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_decrypt'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION encrypt_iv(bytea, bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_encrypt_iv'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION decrypt_iv(bytea, bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_decrypt_iv'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION gen_random_bytes(int4)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_random_bytes'
+LANGUAGE C VOLATILE STRICT;
+
+CREATE FUNCTION gen_random_uuid()
+RETURNS uuid
+AS 'MODULE_PATHNAME', 'pg_random_uuid'
+LANGUAGE C VOLATILE;
+
+--
+-- pgp_sym_encrypt(data, key)
+--
+CREATE FUNCTION pgp_sym_encrypt(text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_text'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pgp_sym_encrypt_bytea(bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_bytea'
+LANGUAGE C STRICT;
+
+--
+-- pgp_sym_encrypt(data, key, args)
+--
+CREATE FUNCTION pgp_sym_encrypt(text, text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_text'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pgp_sym_encrypt_bytea(bytea, text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_bytea'
+LANGUAGE C STRICT;
+
+--
+-- pgp_sym_decrypt(data, key)
+--
+CREATE FUNCTION pgp_sym_decrypt(bytea, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_text'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_sym_decrypt_bytea(bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_bytea'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- pgp_sym_decrypt(data, key, args)
+--
+CREATE FUNCTION pgp_sym_decrypt(bytea, text, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_text'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_sym_decrypt_bytea(bytea, text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_bytea'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- pgp_pub_encrypt(data, key)
+--
+CREATE FUNCTION pgp_pub_encrypt(text, bytea)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_text'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pgp_pub_encrypt_bytea(bytea, bytea)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_bytea'
+LANGUAGE C STRICT;
+
+--
+-- pgp_pub_encrypt(data, key, args)
+--
+CREATE FUNCTION pgp_pub_encrypt(text, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_text'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pgp_pub_encrypt_bytea(bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_bytea'
+LANGUAGE C STRICT;
+
+--
+-- pgp_pub_decrypt(data, key)
+--
+CREATE FUNCTION pgp_pub_decrypt(bytea, bytea)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_text'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_pub_decrypt_bytea(bytea, bytea)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_bytea'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- pgp_pub_decrypt(data, key, psw)
+--
+CREATE FUNCTION pgp_pub_decrypt(bytea, bytea, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_text'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_pub_decrypt_bytea(bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_bytea'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- pgp_pub_decrypt(data, key, psw, arg)
+--
+CREATE FUNCTION pgp_pub_decrypt(bytea, bytea, text, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_text'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_pub_decrypt_bytea(bytea, bytea, text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_bytea'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- PGP key ID
+--
+CREATE FUNCTION pgp_key_id(bytea)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_key_id_w'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- pgp armor
+--
+CREATE FUNCTION armor(bytea)
+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 7f79d04..bb6885b 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 ec647f0..24eb42f 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 5d2d465..67eb3c5 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,31 @@ pg_armor(PG_FUNCTION_ARGS)
text *res;
int data_len;
StringInfoData buf;
+ int num_headers;
+ char **keys,
+ **values;
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;
+ keys = NULL;
+ values = NULL;
+ }
+ 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 +997,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 cecd181..398f21b 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 71ffba2..6e7a43e 100644
--- a/contrib/pgcrypto/sql/pgp-armor.sql
+++ b/contrib/pgcrypto/sql/pgp-armor.sql
@@ -56,3 +56,144 @@ 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-----
+');
+
+-- 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 544a1f8..f0928f8 100644
--- a/doc/src/sgml/pgcrypto.sgml
+++ b/doc/src/sgml/pgcrypto.sgml
@@ -691,13 +691,39 @@ pgp_key_id(bytea) returns text
</indexterm>
<synopsis>
-armor(data bytea) returns text
+armor(data bytea [ , keys text[], values text[] ]) returns text
dearmor(data text) returns bytea
</synopsis>
<para>
These functions wrap/unwrap binary data into PGP ASCII-armor format,
which is basically Base64 with CRC and additional formatting.
</para>
+
+ <para>
+ If the <parameter>keys</> and <parameter>values</> arrays are specified,
+ an <firstterm>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.
+ </para>
+ </sect3>
+
+ <sect3>
+ <title><function>pgp_armor_headers</function></title>
+
+ <indexterm>
+ <primary>pgp_armor_headers</primary>
+ </indexterm>
+
+<synopsis>
+pgp_armor_headers(data text, key out text, value out text) returns setof record
+</synopsis>
+ <para>
+ <function>pgp_armor_headers()</> extracts the armor headers from
+ <parameter>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.
+ </para>
</sect3>
<sect3>
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers