On 09/30/2014 06:39 PM, Marko Tiikkaja wrote:
On 9/30/14 5:17 PM, Heikki Linnakangas wrote:
I'm actually now leaning towards providing just a single function,
pgp_armor_headers(text, key OUT text, value OUT text), which returns all
the keys and values. That gives maximum flexibility, and leaves it up to
the user to decide what to do with duplicate keys. It's pretty easy to
use that to extract just a single header, too:
<snip>
What do you think? Attached patch implements that, but the docs and
regression tests now need adjustment.
(You forgot the patch, but I can imagine what it would have been.)
Oops.
I'm not exactly sure to be honest. I would personally like to see a
simple function for extracting a single header value in a scalar context
without having to deal with all the pain of SRFs, multiple matches and
no matches. Sure, that got a lot better in 9.3 with LATERAL but it's
still way inferior to pgp_armor_header().
I can also see why someone would argue that I should just create the
function myself and that it doesn't have to be shipped with postgres.
But on the other hand, this is already an extension one has to
explicitly go and CREATE, and that considered one more function probably
wouldn't hurt too much.
Yeah, building the function to extract a single value is pretty simple
once you have the set-returning function:
create function pgp_armor_header(armor text, key text) returns text
language sql as $$
select string_agg(value, ' ') from pgp_armor_headers($1) where key = $2
$$;
I spent a little time cleaning up the regression tests and docs, and
ended up with the attached. But then I realized that there's a problem
with UTF-8 conversion in the armor() function. It returns the armored
blob as text, but forcibly converts the keys and values to UTF-8. That's
not cool, because you will get invalidly encoded strings into the
database, if you use the function while connected to a database that
uses some other encoding than UTF-8.
RFC4880 says that the headers are in UTF-8, but armor() cannot safely
return UTF-8 encoded text unless the database's encoding is also UTF-8.
It also rightly says that using anything else than plain ASCII, even
though nominally it's UTF-8, is a bad idea, because the whole point of
ASCII-armoring is to make the format safe from encoding conversions.
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.
- Heikki
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..bcf9590 100644
--- a/contrib/pgcrypto/expected/pgp-armor.out
+++ b/contrib/pgcrypto/expected/pgp-armor.out
@@ -102,3 +102,245 @@ 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)
+
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..5bd02e7 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,108 @@ 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 *));
+
+ /* Read all the header lines into the 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 */
+ 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..2c5325f 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.
@@ -816,6 +820,73 @@ 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 containing UTF-8 strings.
+ */
+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;
+
+ if (key_nulls[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("null value not allowed for header key")));
+ if (val_nulls[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+ errmsg("null value not allowed for header value")));
+
+ v = TextDatumGetCString(key_datums[i]);
+ keys[i] = pg_server_to_any(v, strlen(v), PG_UTF8);
+ v = TextDatumGetCString(val_datums[i]);
+ values[i] = pg_server_to_any(v, strlen(v), PG_UTF8);
+ }
+
+ *p_keys = keys;
+ *p_values = values;
+ return key_count;
+}
+
Datum
pg_armor(PG_FUNCTION_ARGS)
{
@@ -823,13 +894,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 +957,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..2dd6f08 100644
--- a/contrib/pgcrypto/sql/pgp-armor.sql
+++ b/contrib/pgcrypto/sql/pgp-armor.sql
@@ -56,3 +56,141 @@ 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"}');
diff --git a/doc/src/sgml/pgcrypto.sgml b/doc/src/sgml/pgcrypto.sgml
index 544a1f8..fb75994 100644
--- a/doc/src/sgml/pgcrypto.sgml
+++ b/doc/src/sgml/pgcrypto.sgml
@@ -691,13 +691,38 @@ 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 are converted to UTF-8.
+ </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. All the keys and values are assumed to be in UTF-8; if any
+ of them is not valid UTF-8, an error is raised.
+ </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