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

Reply via email to