2005-07-10 21:46:29 +08:00
|
|
|
/*
|
|
|
|
* pgp-armor.c
|
|
|
|
* PGP ascii-armor.
|
|
|
|
*
|
|
|
|
* Copyright (c) 2005 Marko Kreen
|
|
|
|
* All rights reserved.
|
|
|
|
*
|
|
|
|
* Redistribution and use in source and binary forms, with or without
|
|
|
|
* modification, are permitted provided that the following conditions
|
|
|
|
* are met:
|
|
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
|
|
* notice, this list of conditions and the following disclaimer.
|
|
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
|
|
* documentation and/or other materials provided with the distribution.
|
|
|
|
*
|
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
2014-05-07 00:12:18 +08:00
|
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
2005-07-10 21:46:29 +08:00
|
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
|
|
* SUCH DAMAGE.
|
|
|
|
*
|
2010-09-21 04:08:53 +08:00
|
|
|
* contrib/pgcrypto/pgp-armor.c
|
2005-07-10 21:46:29 +08:00
|
|
|
*/
|
|
|
|
|
2005-07-11 23:07:59 +08:00
|
|
|
#include "postgres.h"
|
2005-07-10 21:46:29 +08:00
|
|
|
|
|
|
|
#include "px.h"
|
|
|
|
#include "pgp.h"
|
|
|
|
|
|
|
|
/*
|
|
|
|
* BASE64 - duplicated :(
|
|
|
|
*/
|
|
|
|
|
|
|
|
static const unsigned char _base64[] =
|
2005-10-15 10:49:52 +08:00
|
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
2005-07-10 21:46:29 +08:00
|
|
|
|
|
|
|
static int
|
|
|
|
b64_encode(const uint8 *src, unsigned len, uint8 *dst)
|
|
|
|
{
|
|
|
|
uint8 *p,
|
|
|
|
*lend = dst + 76;
|
|
|
|
const uint8 *s,
|
|
|
|
*end = src + len;
|
|
|
|
int pos = 2;
|
|
|
|
unsigned long buf = 0;
|
|
|
|
|
|
|
|
s = src;
|
|
|
|
p = dst;
|
|
|
|
|
|
|
|
while (s < end)
|
|
|
|
{
|
|
|
|
buf |= *s << (pos << 3);
|
|
|
|
pos--;
|
|
|
|
s++;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* write it out
|
|
|
|
*/
|
|
|
|
if (pos < 0)
|
|
|
|
{
|
|
|
|
*p++ = _base64[(buf >> 18) & 0x3f];
|
|
|
|
*p++ = _base64[(buf >> 12) & 0x3f];
|
|
|
|
*p++ = _base64[(buf >> 6) & 0x3f];
|
|
|
|
*p++ = _base64[buf & 0x3f];
|
|
|
|
|
|
|
|
pos = 2;
|
|
|
|
buf = 0;
|
|
|
|
}
|
|
|
|
if (p >= lend)
|
|
|
|
{
|
|
|
|
*p++ = '\n';
|
|
|
|
lend = p + 76;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (pos != 2)
|
|
|
|
{
|
|
|
|
*p++ = _base64[(buf >> 18) & 0x3f];
|
|
|
|
*p++ = _base64[(buf >> 12) & 0x3f];
|
|
|
|
*p++ = (pos == 0) ? _base64[(buf >> 6) & 0x3f] : '=';
|
|
|
|
*p++ = '=';
|
|
|
|
}
|
|
|
|
|
|
|
|
return p - dst;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* probably should use lookup table */
|
|
|
|
static int
|
|
|
|
b64_decode(const uint8 *src, unsigned len, uint8 *dst)
|
|
|
|
{
|
|
|
|
const uint8 *srcend = src + len,
|
2005-10-15 10:49:52 +08:00
|
|
|
*s = src;
|
2005-07-10 21:46:29 +08:00
|
|
|
uint8 *p = dst;
|
|
|
|
char c;
|
|
|
|
unsigned b = 0;
|
|
|
|
unsigned long buf = 0;
|
|
|
|
int pos = 0,
|
2005-10-15 10:49:52 +08:00
|
|
|
end = 0;
|
2005-07-10 21:46:29 +08:00
|
|
|
|
|
|
|
while (s < srcend)
|
|
|
|
{
|
|
|
|
c = *s++;
|
|
|
|
if (c >= 'A' && c <= 'Z')
|
|
|
|
b = c - 'A';
|
|
|
|
else if (c >= 'a' && c <= 'z')
|
|
|
|
b = c - 'a' + 26;
|
|
|
|
else if (c >= '0' && c <= '9')
|
|
|
|
b = c - '0' + 52;
|
|
|
|
else if (c == '+')
|
|
|
|
b = 62;
|
|
|
|
else if (c == '/')
|
|
|
|
b = 63;
|
|
|
|
else if (c == '=')
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
* end sequence
|
|
|
|
*/
|
|
|
|
if (!end)
|
|
|
|
{
|
|
|
|
if (pos == 2)
|
|
|
|
end = 1;
|
|
|
|
else if (pos == 3)
|
|
|
|
end = 2;
|
|
|
|
else
|
|
|
|
return PXE_PGP_CORRUPT_ARMOR;
|
|
|
|
}
|
|
|
|
b = 0;
|
|
|
|
}
|
|
|
|
else if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
|
|
|
|
continue;
|
|
|
|
else
|
|
|
|
return PXE_PGP_CORRUPT_ARMOR;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* add it to buffer
|
|
|
|
*/
|
|
|
|
buf = (buf << 6) + b;
|
|
|
|
pos++;
|
|
|
|
if (pos == 4)
|
|
|
|
{
|
|
|
|
*p++ = (buf >> 16) & 255;
|
|
|
|
if (end == 0 || end > 1)
|
|
|
|
*p++ = (buf >> 8) & 255;
|
|
|
|
if (end == 0 || end > 2)
|
|
|
|
*p++ = buf & 255;
|
|
|
|
buf = 0;
|
|
|
|
pos = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pos != 0)
|
|
|
|
return PXE_PGP_CORRUPT_ARMOR;
|
|
|
|
return p - dst;
|
|
|
|
}
|
|
|
|
|
|
|
|
static unsigned
|
|
|
|
b64_enc_len(unsigned srclen)
|
|
|
|
{
|
|
|
|
/*
|
|
|
|
* 3 bytes will be converted to 4, linefeed after 76 chars
|
|
|
|
*/
|
|
|
|
return (srclen + 2) * 4 / 3 + srclen / (76 * 3 / 4);
|
|
|
|
}
|
|
|
|
|
|
|
|
static unsigned
|
|
|
|
b64_dec_len(unsigned srclen)
|
|
|
|
{
|
|
|
|
return (srclen * 3) >> 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* PGP armor
|
|
|
|
*/
|
|
|
|
|
2014-10-01 20:56:26 +08:00
|
|
|
static const char *armor_header = "-----BEGIN PGP MESSAGE-----\n";
|
2005-07-10 21:46:29 +08:00
|
|
|
static const char *armor_footer = "\n-----END PGP MESSAGE-----\n";
|
|
|
|
|
|
|
|
/* CRC24 implementation from rfc2440 */
|
|
|
|
#define CRC24_INIT 0x00b704ceL
|
|
|
|
#define CRC24_POLY 0x01864cfbL
|
|
|
|
static long
|
|
|
|
crc24(const uint8 *data, unsigned len)
|
|
|
|
{
|
|
|
|
unsigned crc = CRC24_INIT;
|
|
|
|
int i;
|
|
|
|
|
|
|
|
while (len--)
|
|
|
|
{
|
|
|
|
crc ^= (*data++) << 16;
|
|
|
|
for (i = 0; i < 8; i++)
|
|
|
|
{
|
|
|
|
crc <<= 1;
|
|
|
|
if (crc & 0x1000000)
|
|
|
|
crc ^= CRC24_POLY;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return crc & 0xffffffL;
|
|
|
|
}
|
|
|
|
|
2014-09-25 21:32:27 +08:00
|
|
|
void
|
2014-10-01 20:56:26 +08:00
|
|
|
pgp_armor_encode(const uint8 *src, unsigned len, StringInfo dst,
|
|
|
|
int num_headers, char **keys, char **values)
|
2005-07-10 21:46:29 +08:00
|
|
|
{
|
2014-10-01 20:56:26 +08:00
|
|
|
int n;
|
2014-09-25 21:32:27 +08:00
|
|
|
int res;
|
|
|
|
unsigned b64len;
|
2005-07-10 21:46:29 +08:00
|
|
|
unsigned crc = crc24(src, len);
|
|
|
|
|
2014-09-25 21:32:27 +08:00
|
|
|
appendStringInfoString(dst, armor_header);
|
2005-07-10 21:46:29 +08:00
|
|
|
|
2014-10-01 20:56:26 +08:00
|
|
|
for (n = 0; n < num_headers; n++)
|
|
|
|
appendStringInfo(dst, "%s: %s\n", keys[n], values[n]);
|
|
|
|
appendStringInfoChar(dst, '\n');
|
|
|
|
|
2014-09-25 21:32:27 +08:00
|
|
|
/* make sure we have enough room to b64_encode() */
|
|
|
|
b64len = b64_enc_len(len);
|
|
|
|
enlargeStringInfo(dst, (int) b64len);
|
2014-10-01 20:56:26 +08:00
|
|
|
|
2014-09-25 21:32:27 +08:00
|
|
|
res = b64_encode(src, len, (uint8 *) dst->data + dst->len);
|
|
|
|
if (res > b64len)
|
|
|
|
elog(FATAL, "overflow - encode estimate too small");
|
|
|
|
dst->len += res;
|
2005-07-10 21:46:29 +08:00
|
|
|
|
2014-09-25 21:32:27 +08:00
|
|
|
if (*(dst->data + dst->len - 1) != '\n')
|
|
|
|
appendStringInfoChar(dst, '\n');
|
2005-07-10 21:46:29 +08:00
|
|
|
|
2014-09-25 21:32:27 +08:00
|
|
|
appendStringInfoChar(dst, '=');
|
|
|
|
appendStringInfoChar(dst, _base64[(crc >> 18) & 0x3f]);
|
|
|
|
appendStringInfoChar(dst, _base64[(crc >> 12) & 0x3f]);
|
|
|
|
appendStringInfoChar(dst, _base64[(crc >> 6) & 0x3f]);
|
|
|
|
appendStringInfoChar(dst, _base64[crc & 0x3f]);
|
2005-07-10 21:46:29 +08:00
|
|
|
|
2014-09-25 21:32:27 +08:00
|
|
|
appendStringInfoString(dst, armor_footer);
|
2005-07-10 21:46:29 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
static const uint8 *
|
|
|
|
find_str(const uint8 *data, const uint8 *data_end, const char *str, int strlen)
|
|
|
|
{
|
|
|
|
const uint8 *p = data;
|
2005-10-15 10:49:52 +08:00
|
|
|
|
2005-07-10 21:46:29 +08:00
|
|
|
if (!strlen)
|
|
|
|
return NULL;
|
|
|
|
if (data_end - data < strlen)
|
|
|
|
return NULL;
|
2005-10-15 10:49:52 +08:00
|
|
|
while (p < data_end)
|
|
|
|
{
|
2005-07-10 21:46:29 +08:00
|
|
|
p = memchr(p, str[0], data_end - p);
|
|
|
|
if (p == NULL)
|
|
|
|
return NULL;
|
|
|
|
if (p + strlen > data_end)
|
|
|
|
return NULL;
|
|
|
|
if (memcmp(p, str, strlen) == 0)
|
|
|
|
return p;
|
|
|
|
p++;
|
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
|
|
find_header(const uint8 *data, const uint8 *datend,
|
2005-10-15 10:49:52 +08:00
|
|
|
const uint8 **start_p, int is_end)
|
2005-07-10 21:46:29 +08:00
|
|
|
{
|
|
|
|
const uint8 *p = data;
|
|
|
|
static const char *start_sep = "-----BEGIN";
|
|
|
|
static const char *end_sep = "-----END";
|
|
|
|
const char *sep = is_end ? end_sep : start_sep;
|
|
|
|
|
|
|
|
/* find header line */
|
|
|
|
while (1)
|
|
|
|
{
|
|
|
|
p = find_str(p, datend, sep, strlen(sep));
|
|
|
|
if (p == NULL)
|
|
|
|
return PXE_PGP_CORRUPT_ARMOR;
|
|
|
|
/* it must start at beginning of line */
|
|
|
|
if (p == data || *(p - 1) == '\n')
|
|
|
|
break;
|
|
|
|
p += strlen(sep);
|
|
|
|
}
|
|
|
|
*start_p = p;
|
|
|
|
p += strlen(sep);
|
|
|
|
|
|
|
|
/* check if header text ok */
|
|
|
|
for (; p < datend && *p != '-'; p++)
|
|
|
|
{
|
2005-10-15 10:49:52 +08:00
|
|
|
/* various junk can be there, but definitely not line-feed */
|
2005-07-10 21:46:29 +08:00
|
|
|
if (*p >= ' ')
|
|
|
|
continue;
|
|
|
|
return PXE_PGP_CORRUPT_ARMOR;
|
|
|
|
}
|
|
|
|
if (datend - p < 5 || memcmp(p, sep, 5) != 0)
|
|
|
|
return PXE_PGP_CORRUPT_ARMOR;
|
|
|
|
p += 5;
|
|
|
|
|
|
|
|
/* check if at end of line */
|
|
|
|
if (p < datend)
|
|
|
|
{
|
|
|
|
if (*p != '\n' && *p != '\r')
|
|
|
|
return PXE_PGP_CORRUPT_ARMOR;
|
|
|
|
if (*p == '\r')
|
|
|
|
p++;
|
|
|
|
if (p < datend && *p == '\n')
|
|
|
|
p++;
|
|
|
|
}
|
|
|
|
return p - *start_p;
|
|
|
|
}
|
|
|
|
|
|
|
|
int
|
2014-09-25 21:32:27 +08:00
|
|
|
pgp_armor_decode(const uint8 *src, int len, StringInfo dst)
|
2005-07-10 21:46:29 +08:00
|
|
|
{
|
|
|
|
const uint8 *p = src;
|
|
|
|
const uint8 *data_end = src + len;
|
|
|
|
long crc;
|
2005-10-15 10:49:52 +08:00
|
|
|
const uint8 *base64_start,
|
|
|
|
*armor_end;
|
2005-07-10 21:46:29 +08:00
|
|
|
const uint8 *base64_end = NULL;
|
|
|
|
uint8 buf[4];
|
|
|
|
int hlen;
|
2014-09-25 21:32:27 +08:00
|
|
|
int blen;
|
2005-07-10 21:46:29 +08:00
|
|
|
int res = PXE_PGP_CORRUPT_ARMOR;
|
|
|
|
|
|
|
|
/* armor start */
|
|
|
|
hlen = find_header(src, data_end, &p, 0);
|
|
|
|
if (hlen <= 0)
|
|
|
|
goto out;
|
|
|
|
p += hlen;
|
|
|
|
|
|
|
|
/* armor end */
|
|
|
|
hlen = find_header(p, data_end, &armor_end, 1);
|
|
|
|
if (hlen <= 0)
|
|
|
|
goto out;
|
2005-10-15 10:49:52 +08:00
|
|
|
|
2005-07-10 21:46:29 +08:00
|
|
|
/* skip comments - find empty line */
|
|
|
|
while (p < armor_end && *p != '\n' && *p != '\r')
|
|
|
|
{
|
|
|
|
p = memchr(p, '\n', armor_end - p);
|
|
|
|
if (!p)
|
|
|
|
goto out;
|
|
|
|
|
|
|
|
/* step to start of next line */
|
|
|
|
p++;
|
|
|
|
}
|
|
|
|
base64_start = p;
|
2005-10-15 10:49:52 +08:00
|
|
|
|
2005-07-10 21:46:29 +08:00
|
|
|
/* find crc pos */
|
|
|
|
for (p = armor_end; p >= base64_start; p--)
|
|
|
|
if (*p == '=')
|
|
|
|
{
|
|
|
|
base64_end = p - 1;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (base64_end == NULL)
|
|
|
|
goto out;
|
|
|
|
|
|
|
|
/* decode crc */
|
|
|
|
if (b64_decode(p + 1, 4, buf) != 3)
|
|
|
|
goto out;
|
2005-10-15 10:49:52 +08:00
|
|
|
crc = (((long) buf[0]) << 16) + (((long) buf[1]) << 8) + (long) buf[2];
|
2005-07-10 21:46:29 +08:00
|
|
|
|
|
|
|
/* decode data */
|
2014-09-25 21:32:27 +08:00
|
|
|
blen = (int) b64_dec_len(len);
|
|
|
|
enlargeStringInfo(dst, blen);
|
|
|
|
res = b64_decode(base64_start, base64_end - base64_start, (uint8 *) dst->data);
|
|
|
|
if (res > blen)
|
|
|
|
elog(FATAL, "overflow - decode estimate too small");
|
|
|
|
if (res >= 0)
|
|
|
|
{
|
|
|
|
if (crc24((uint8 *) dst->data, res) == crc)
|
|
|
|
dst->len += res;
|
|
|
|
else
|
|
|
|
res = PXE_PGP_CORRUPT_ARMOR;
|
|
|
|
}
|
2005-07-10 21:46:29 +08:00
|
|
|
out:
|
|
|
|
return res;
|
|
|
|
}
|
2014-10-01 20:56:26 +08:00
|
|
|
|
|
|
|
/*
|
|
|
|
* 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,
|
2015-05-24 09:35:49 +08:00
|
|
|
*colon;
|
2014-10-01 20:56:26 +08:00
|
|
|
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;
|
|
|
|
}
|