blob: ede043d1078e0bed734c27d728a35ca23ce8113c [file] [log] [blame]
/*-------------------------------------------------------------------------
*
* kmgr_utils.c
* Shared frontend/backend for cluster file encryption
*
* These functions handle reading the wrapped DEK files from the
* file system, and wrapping and unwrapping them. It also handles
* running the cluster_key_command.
*
* Copyright (c) 2021, PostgreSQL Global Development Group
*
* IDENTIFICATION
* src/common/kmgr_utils.c
*
*-------------------------------------------------------------------------
*/
#ifndef FRONTEND
#include "postgres.h"
#else
#include "postgres_fe.h"
#endif
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifdef FRONTEND
#include "common/logging.h"
#endif
#include "common/cryptohash.h"
#include "common/file_perm.h"
#include "common/string.h"
#include "crypto/kmgr.h"
#include "lib/stringinfo.h"
#include "storage/fd.h"
#include "postgres.h"
#include "utils/builtins.h"
#ifndef FRONTEND
#include "pgstat.h"
#include "storage/fd.h"
#endif
#define KMGR_PROMPT_MSG "Enter authentication needed to generate the cluster key: "
#ifdef FRONTEND
static FILE *open_pipe_stream(const char *command);
static int close_pipe_stream(FILE *file);
#endif
static void read_wrapped_data_key(const char *cryptoKeyDir, uint32 id, unsigned char **key_p, int *key_len);
/*
* We need this array in frontend and backend code, so define it here.
* You can only add to the end of this array since the array index is
* stored in pg_control.
*/
encryption_method encryption_methods[NUM_ENCRYPTION_METHODS] = {
{"", 0},
{"AES128", 128},
{"AES192", 192},
{"AES256", 256},
{"SM4", 128}
};
/* This maps wrapped key filesnames to their array slots */
char *wkey_filenames[KMGR_NUM_DATA_KEYS] = {
"relation",
"wal"
};
/*
* Wrap the given CryptoKey.
*
* Returns true and writes encrypted/wrapped/padded data to 'out', and the length
* of the result to outlen, if successful.
*
* Otherwise returns false. The caller must allocate sufficient space
* for cipher data calculated by using KmgrSizeOfCipherText(). Please note that
* this function modifies 'out' data even on failure.
*/
bool
kmgr_wrap_data_key(PgCipherCtx *ctx, CryptoKey *in, unsigned char *out, int *outlen)
{
Assert(ctx && in && out);
if (!pg_cipher_keywrap(ctx, (unsigned char *) in, sizeof(CryptoKey), out, outlen))
return false;
return true;
}
/*
* Decrypt the given data. Return true and set plain text data to `out` if
* successful. Otherwise return false. The caller must allocate sufficient
* space for cipher data calculated by using KmgrSizeOfPlainText(). Please
* note that this function modifies 'out' data even on failure.
*/
bool
kmgr_unwrap_data_key(PgCipherCtx *ctx, unsigned char *in, int inlen, CryptoKey *out)
{
int outlen;
int out_buffer_len;
unsigned char *out_buffer;
/*
* When call EVP_DecryptUpdate,
* We need to alloc enough buffer
* More detail info see
* https://www.openssl.org/docs/man3.1/man3/EVP_DecryptUpdate.html
*/
out_buffer_len = pg_cipher_blocksize(ctx) + inlen;
out_buffer = (unsigned char *)palloc0(out_buffer_len);
Assert(ctx && in && out);
if (!pg_cipher_keyunwrap(ctx, in, inlen, (unsigned char *) out_buffer, &outlen))
return false;
Assert(outlen == sizeof(CryptoKey));
memcpy(out, out_buffer, sizeof(CryptoKey));
pfree(out_buffer);
return true;
}
/*
* Verify the correctness of the given cluster key by unwrapping the given keys.
* If the given cluster key is correct we set unwrapped keys to out_keys and return
* true. Otherwise return false. Please note that this function changes the
* contents of out_keys even on failure. Both in_keys and out_keys must be the
* same length.
*/
bool
kmgr_verify_cluster_key(unsigned char *cluster_key,
unsigned char **in_keys, int *key_lens, CryptoKey *out_keys)
{
PgCipherCtx *ctx;
/* Create decryption context with the KEK. */
ctx = pg_cipher_ctx_create(PG_CIPHER_AES_KWP, cluster_key,
KMGR_CLUSTER_KEY_LEN, false);
/* unwrap each DEK */
for (int i = 0; i < KMGR_NUM_DATA_KEYS; i++)
{
if (!kmgr_unwrap_data_key(ctx, in_keys[i], key_lens[i], &(out_keys[i])))
{
/* The cluster key is not correct */
pg_cipher_ctx_free(ctx);
return false;
}
explicit_bzero(in_keys[i], key_lens[i]);
}
/* The cluster key is correct, free the cipher context */
pg_cipher_ctx_free(ctx);
return true;
}
/*
* Run cluster key command.
*
* Substitute %d for directory, %p for prompt, %R for file descriptor.
*
* The result will be put in buffer buf, which is of size "size".
* The return value is the length of the actual result.
*/
int
kmgr_run_cluster_key_command(char *cluster_key_command, char *buf,
int size, char *dir, int terminal_fd)
{
StringInfoData command;
const char *sp;
FILE *fh;
int pclose_rc;
size_t len = 0;
buf[0] = '\0';
Assert(size > 0);
/*
* Build the command to be executed.
*/
initStringInfo(&command);
for (sp = cluster_key_command; *sp; sp++)
{
if (*sp == '%')
{
switch (sp[1])
{
/* directory */
case 'd':
{
char *nativePath;
sp++;
/*
* This needs to use a placeholder to not modify the
* input with the conversion done via
* make_native_path().
*/
nativePath = pstrdup(dir);
make_native_path(nativePath);
appendStringInfoString(&command, nativePath);
pfree(nativePath);
break;
}
/* prompt string */
case 'p':
appendStringInfoString(&command, " ");
sp++;
appendStringInfoString(&command, KMGR_PROMPT_MSG);
break;
/* file descriptor number */
case 'R':
{
char fd_str[20];
if (terminal_fd == -1)
{
#ifdef FRONTEND
pg_fatal("cluster key command referenced %%R, but --authprompt not specified");
#else
ereport(ERROR,
(errcode(ERRCODE_INTERNAL_ERROR),
errmsg("cluster key command referenced %%R, but --authprompt not specified")));
#endif
}
appendStringInfoString(&command, " ");
sp++;
snprintf(fd_str, sizeof(fd_str), "%d", terminal_fd);
appendStringInfoString(&command, fd_str);
break;
}
/* literal "%" */
case '%':
/* convert %% to a single % */
sp++;
appendStringInfoChar(&command, *sp);
break;
default:
/* otherwise treat the % as not special */
appendStringInfoChar(&command, *sp);
break;
}
}
else
{
appendStringInfoChar(&command, *sp);
}
}
#ifdef FRONTEND
fh = open_pipe_stream(command.data);
if (fh == NULL)
{
pg_fatal("could not execute command \"%s\": %m",
command.data);
}
#else
fh = OpenPipeStream(command.data, "r");
if (fh == NULL)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not execute command \"%s\": %m",
command.data)));
#endif
if (!fgets(buf, size, fh))
{
if (ferror(fh))
{
#ifdef FRONTEND
pg_fatal("could not read from command \"%s\": %m",
command.data);
#else
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not read from command \"%s\": %m",
command.data)));
#endif
}
}
#ifdef FRONTEND
pclose_rc = close_pipe_stream(fh);
#else
pclose_rc = ClosePipeStream(fh);
#endif
if (pclose_rc == -1)
{
#ifdef FRONTEND
pg_fatal("could not close pipe to external command: %m");
#else
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not close pipe to external command: %m")));
#endif
}
else if (pclose_rc != 0)
{
#ifdef FRONTEND
pg_fatal("command \"%s\" failed", command.data);
#else
ereport(ERROR,
(errcode_for_file_access(),
errmsg("command \"%s\" failed",
command.data),
errdetail_internal("%s", wait_result_to_str(pclose_rc))));
#endif
}
/* strip trailing newline and carriage returns */
len = pg_strip_crlf(buf);
pfree(command.data);
return len;
}
#ifdef FRONTEND
static FILE *
open_pipe_stream(const char *command)
{
FILE *res;
#ifdef WIN32
size_t cmdlen = strlen(command);
char *buf;
int save_errno;
buf = malloc(cmdlen + 2 + 1);
if (buf == NULL)
{
errno = ENOMEM;
return NULL;
}
buf[0] = '"';
memcpy(&buf[1], command, cmdlen);
buf[cmdlen + 1] = '"';
buf[cmdlen + 2] = '\0';
res = _popen(buf, "r");
save_errno = errno;
free(buf);
errno = save_errno;
#else
res = popen(command, "r");
#endif /* WIN32 */
return res;
}
static int
close_pipe_stream(FILE *file)
{
#ifdef WIN32
return _pclose(file);
#else
return pclose(file);
#endif /* WIN32 */
}
#endif /* FRONTEND */
/*
* Reads the keys at path.
*
* This routine simply reads in the raw encrypted/wrapped keys;
* it does not handle any decryption, see kmgr_key_unwrap().
*
* For each key returned, the key and key length are returned
* in the keys and key_lens arrays respectfully.
*
* Note that keys and key_lens must be allocated before calling
* this function as arrays of at least KMGR_NUM_DATA_KEYS length.
*/
void
kmgr_read_wrapped_data_keys(const char *path, unsigned char **keys, int *key_lens)
{
/* StaticAssertStmt(lengthof(wkey_filenames) == KMGR_NUM_DATA_KEYS,
"wkey_filenames[] must match KMGR_NUM_DATA_KEYS");
*/
StaticAssertStmt(1 == 1,
"wkey_filenames[] must match KMGR_NUM_DATA_KEYS");
for (int id = 0; id < KMGR_NUM_DATA_KEYS; id++)
read_wrapped_data_key(path, id, &(keys[id]), &(key_lens[id]));
return;
}
/* Read a wrapped DEK file */
static void
read_wrapped_data_key(const char *cryptoKeyDir, uint32 id, unsigned char **key_p, int *key_len)
{
char path[MAXPGPATH];
int fd;
int r;
struct stat st;
CryptoKeyFilePath(path, cryptoKeyDir, id);
#ifndef FRONTEND
if ((fd = OpenTransientFile(path, O_RDONLY | PG_BINARY)) == -1)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not open file \"%s\" for reading: %m",
path)));
else if (fstat(fd, &st))
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not stat file \"%s\": %m",
path)));
#else
if ((fd = open(path, O_RDONLY | PG_BINARY, 0)) == -1)
pg_fatal("could not open file \"%s\" for reading: %m",
path);
else if (fstat(fd, &st))
pg_fatal("could not stat file \"%s\": %m",
path);
#endif
*key_len = st.st_size;
#ifndef FRONTEND
pgstat_report_wait_start(WAIT_EVENT_KEY_FILE_READ);
#endif
*key_p = (unsigned char *) palloc0(*key_len);
/* Get key bytes */
r = read(fd, *key_p, *key_len);
if (r != *key_len)
{
if (r < 0)
{
#ifndef FRONTEND
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not read file \"%s\": %m", path)));
#else
pg_fatal("could not read file \"%s\": %m", path);
#endif
}
else
{
#ifndef FRONTEND
ereport(ERROR,
(errcode(ERRCODE_DATA_CORRUPTED),
errmsg("could not read file \"%s\": read %d of %u",
path, r, *key_len)));
#else
pg_fatal("could not read file \"%s\": read %d of %u",
path, r, *key_len);
#endif
}
}
#ifndef FRONTEND
pgstat_report_wait_end();
#endif
#ifndef FRONTEND
if (CloseTransientFile(fd) != 0)
ereport(ERROR,
(errcode_for_file_access(),
errmsg("could not close file \"%s\": %m",
path)));
#else
if (close(fd) != 0)
pg_fatal("could not close file \"%s\": %m", path);
#endif
}