/*------------------------------------------------------------------------- * * kmgr_utils.c * Shared frontend/backend for cluster file encryption * * Copyright (c) 2020, PostgreSQL Global Development Group * * IDENTIFICATION * src/common/kmgr_utils.c * *------------------------------------------------------------------------- */ #ifndef FRONTEND #include "postgres.h" #else #include "postgres_fe.h" #endif #include #include #ifdef FRONTEND #include "common/logging.h" #endif #include "common/cryptohash.h" #include "common/file_perm.h" #include "common/kmgr_utils.h" #include "common/hex_decode.h" #include "common/string.h" #include "crypto/kmgr.h" #include "lib/stringinfo.h" #include "postmaster/postmaster.h" #include "storage/fd.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_one_keyfile(const char *dataDir, uint32 id, CryptoKey *key_p); /* * Encrypt the given data. Return true and set encrypted data to 'out' if * success. Otherwise return 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 case. */ bool kmgr_wrap_key(PgCipherCtx *ctx, CryptoKey *in, CryptoKey *out) { int len, enclen; unsigned char iv[sizeof(in->pgkey_id) + sizeof(in->counter)]; Assert(ctx && in && out); /* Get the actual length of the key we are wrapping */ memcpy(&len, in->encrypted_key, sizeof(len)); /* Key ID remains the same */ out->pgkey_id = in->pgkey_id; /* Increment the counter */ out->counter = in->counter + 1; /* Construct the IV we are going to use, see kmgr_utils.h */ memcpy(iv, &out->pgkey_id, sizeof(out->pgkey_id)); memcpy(iv + sizeof(out->pgkey_id), &out->counter, sizeof(out->counter)); if (!pg_cipher_encrypt(ctx, in->encrypted_key, /* Plaintext source, key length + key */ sizeof(in->encrypted_key), /* Full data length */ out->encrypted_key, /* Ciphertext result */ &enclen, /* Resulting length, must match input for us */ iv, /* Generated IV from above */ sizeof(iv), /* Length of the IV */ out->tag, /* Resulting tag */ sizeof(out->tag))) /* Length of our tag */ return false; Assert(enclen == sizeof(in->encrypted_key)); return true; } /* * Decrypt the given Data. Return true and set plain text data to `out` if * success. 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 case. */ bool kmgr_unwrap_key(PgCipherCtx *ctx, CryptoKey *in, CryptoKey *out) { int declen; unsigned char iv[sizeof(in->pgkey_id) + sizeof(in->counter)]; Assert(ctx && in && out); out->pgkey_id = in->pgkey_id; out->counter = in->counter; memcpy(out->tag, in->tag, sizeof(in->tag)); /* Construct the IV we are going to use, see kmgr_utils.h */ memcpy(iv, &out->pgkey_id, sizeof(out->pgkey_id)); memcpy(iv + sizeof(out->pgkey_id), &out->counter, sizeof(out->counter)); /* Decrypt encrypted data */ if (!pg_cipher_decrypt(ctx, in->encrypted_key, /* Encrypted source */ sizeof(in->encrypted_key), /* Length of encrypted data */ out->encrypted_key, /* Plaintext result */ &declen, /* Length of plaintext */ iv, /* IV we constructed above */ sizeof(iv), /* Size of our IV */ in->tag, /* Tag which will be verified */ sizeof(in->tag))) /* Size of our tag */ return false; Assert(declen == sizeof(in->encrypted_key)); 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, nkey. */ bool kmgr_verify_cluster_key(unsigned char *cluster_key, CryptoKey *in_keys, CryptoKey *out_keys, int nkeys) { PgCipherCtx *ctx; /* * Create decryption context with cluster KEK. */ ctx = pg_cipher_ctx_create(PG_CIPHER_AES_GCM, cluster_key, KMGR_CLUSTER_KEY_LEN, false); for (int i = 0; i < nkeys; i++) { if (!kmgr_unwrap_key(ctx, &(in_keys[i]), &(out_keys[i]))) { /* The cluster key is not correct */ pg_cipher_ctx_free(ctx); return false; } explicit_bzero(&(in_keys[i]), sizeof(in_keys[i])); } /* The cluster key is correct, free the cipher context */ pg_cipher_ctx_free(ctx); return true; } /* * Run cluster key command. * * prompt will be substituted for %p, file descriptor for %R * * 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) { 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]) { 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; } case 'p': sp++; appendStringInfoString(&command, KMGR_PROMPT_MSG); break; case 'R': { char fd_str[20]; if (terminal_fd == -1) { #ifdef FRONTEND pg_log_fatal("cluster key command referenced %%R, but --authprompt not specified"); exit(EXIT_FAILURE); #else ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("cluster key command referenced %%R, but --authprompt not specified"))); #endif } sp++; snprintf(fd_str, sizeof(fd_str), "%d", terminal_fd); appendStringInfoString(&command, fd_str); break; } 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_log_fatal("could not execute command \"%s\": %m", command.data); exit(EXIT_FAILURE); } #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_log_fatal("could not read from command \"%s\": %m", command.data); exit(EXIT_FAILURE); #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_log_fatal("could not close pipe to external command: %m"); exit(EXIT_FAILURE); #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_log_fatal("command \"%s\" failed", command.data); exit(EXIT_FAILURE); #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 return */ 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] = '"'; mempcy(&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 */ CryptoKey * kmgr_get_cryptokeys(const char *path, int *nkeys) { struct dirent *de; DIR *dir; CryptoKey *keys; #ifndef FRONTEND if ((dir = AllocateDir(path)) == NULL) ereport(ERROR, (errcode_for_file_access(), errmsg("could not open directory \"%s\": %m", path))); #else if ((dir = opendir(path)) == NULL) pg_log_fatal("could not open directory \"%s\": %m", path); #endif keys = (CryptoKey *) palloc0(sizeof(CryptoKey) * KMGR_MAX_INTERNAL_KEYS); *nkeys = 0; #ifndef FRONTEND while ((de = ReadDir(dir, LIVE_KMGR_DIR)) != NULL) #else while ((de = readdir(dir)) != NULL) #endif { if (strspn(de->d_name, "0123456789") == strlen(de->d_name)) { uint32 id = strtoul(de->d_name, NULL, 10); if (id < 0 || id >= KMGR_MAX_INTERNAL_KEYS) { #ifndef FRONTEND elog(ERROR, "invalid cryptographic key identifier %u", id); #else pg_log_fatal("invalid cryptographic key identifier %u", id); #endif } if (*nkeys >= KMGR_MAX_INTERNAL_KEYS) { #ifndef FRONTEND elog(ERROR, "too many cryptographic keys"); #else pg_log_fatal("too many cryptographic keys"); #endif } read_one_keyfile(path, id, &(keys[id])); (*nkeys)++; } } #ifndef FRONTEND FreeDir(dir); #else closedir(dir); #endif return keys; } static void read_one_keyfile(const char *cryptoKeyDir, uint32 id, CryptoKey *key_p) { char path[MAXPGPATH]; int fd; int r; 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 ((fd = open(path, O_RDONLY | PG_BINARY, 0)) == -1) pg_log_fatal("could not open file \"%s\" for reading: %m", path); #endif #ifndef FRONTEND pgstat_report_wait_start(WAIT_EVENT_KEY_FILE_READ); #endif /* Get key bytes */ r = read(fd, key_p, sizeof(CryptoKey)); if (r != sizeof(CryptoKey)) { if (r < 0) { #ifndef FRONTEND ereport(ERROR, (errcode_for_file_access(), errmsg("could not read file \"%s\": %m", path))); #else pg_log_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 %zu", path, r, sizeof(CryptoKey)))); #else pg_log_fatal("could not read file \"%s\": read %d of %zu", path, r, sizeof(CryptoKey)); #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_log_fatal("could not close file \"%s\": %m", path); #endif }