diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml new file mode 100644 index 4b60382..ecc1a65 *** a/doc/src/sgml/config.sgml --- b/doc/src/sgml/config.sgml *************** COPY postgres_log FROM '/full/path/to/lo *** 7816,7821 **** --- 7816,7867 ---- + + Cluster File Encryption + + + + cluster_passphrase_command (string) + + cluster_passphrase_command configuration parameter + + + + + This option specifies an external command to obtain the passphrase + for cluster file encryption during server initialization and + server start. + + + The command can prompt for the passphrase or PIN from the terminal + if is used, and must exit with + code 0. In the parameter value, %R represents + the file descriptor number opened to the terminal that started + the server. This is useful for prompting from the terminal. + A file descriptor is only available if enabled at server start. If + %R is used and no file descriptor is available, + the server will not start. Value %P is replaced + by a pre-defined prompt string. Value %d is + replaced by the directory containing the keys; this is useful + if the command must create files with the keys, e.g., to store a + passphrase encryped by a key stored in a hardware security module. + (Write %% for a literal %.) + Note that the prompt string will probably contain whitespace, so + be sure to quote its use adequately. A single newline is stripped + from the end of the output if present. The output passphrase must + be at least 64 bytes. + + + This parameter can only be set by + initdb, in the + postgresql.conf file, or on the server + command line. + + + + + + Client Connection Defaults *************** dynamic_library_path = 'C:\tools\postgre *** 9636,9641 **** --- 9682,9703 ---- + + + file_encryption_keylen (boolean) + + Cluster file encryption key length + + + + + Reports the bit length of the cluster file + encryption key, or zero if disabled. See for more + information. + + + data_directory_mode (integer) diff --git a/doc/src/sgml/database-encryption.sgml b/doc/src/sgml/database-encryption.sgml new file mode 100644 index ...f0788ec *** a/doc/src/sgml/database-encryption.sgml --- b/doc/src/sgml/database-encryption.sgml *************** *** 0 **** --- 1,159 ---- + + + + Cluster File Encryption + + + Cluster File Encryption + + + + The purpose of cluster file encryption is to prevent users with read + access to the directories used to store database files from being able to + access the data stored in those files. For example, when using cluster + file encryption, users who have read access to the cluster directories + for backup purposes will not be able to read the data stored in the + these files. + + + + Cluster file encryption uses two levels of encryption. The first level + is data encryption keys, specifically keys zero and one. Key zero is + the key used to encrypt database heap and index files which are stored in + the file system, plus temporary files created during database operation. + XXX others? Key one is used to encrypt write-ahead log (WAL) files. + Two different keys are used so that primary and standby servers can + use different zero (heap/index/temp) keys, but the same one (WAL) key, + so that these keys can eventually be rotated by switching the primary + to the standby as and then changing the WAL key. + + + + The second level of encryption is a key used to encrypt first-level + keys. This type of key is often referred to as a Key Encryption Key + (KEK). This key is not stored + in the file system, but provided at initdb time and + each time the server is started. This key prevents anyone with access + to the database directories from reading the data because they do not + know the second-level key which encrypted the first-level keys which + encrypted the database cluster files. + + + + Initialization + + + Cluster file encryption is enabled when + PostgreSQL is built + with --with-openssl and is specified + during initdb. The cluster passphrase + provided by the + option during initdb and the one generated + by in the + postgresql.conf must match for the database + cluster to start. Note that the cluster passphrase command + passed to initdb must return a passphrase of at + least 64 bytes and less than 1024 bytes. For example. + + initdb -D dbname --cluster-passphrase-command="cat /path/to/passphrase-file" + + + + + + Internals + + + During the initdb process, if the + is specified, two + data-level encryption keys are created. These two keys are then + encrypted with the passphrase (KEK) supplied by the cluster passphrase + command before being stored in the database directory. The passphrase + must be stored in a trusted key store, such as key vault software, + hardware security module, or supplied from the terminal. + + + + If the PostgreSQL server has + been initialized to require a passphrase, each time the + server is started the postgresql.conf + cluster_passphrase_command command will be executed + and the cluster passphrase retrieved. The data encryption keys in the + pg_cryptokeys directory will then be decrypted + using the supplied passphrase and integrity-checked to see if it + matches the initdb-supplied passphrase. If this check fails, the + server will refuse to start. + + + + + Key Protection + + + Cluster file encryption uses Encryption with Associated Data + (AEAD) to wrap cryptographic keys. In addition + to providing a way to protect confidential data from being revealed, + it provides a way to check the integrity and authenticity of some + associated data. It uses the Encrypt-Then-MAC approach, based on the + Advanced Encryption Standard (AES256) in Cipher Block + Chaining (CBC) mode. It uses a random initialization + vector (IV) and a HMAC-SHA + message authentication code (MAC). + + + + Key management system uses two kinds of cryptographic keys for key wrapping: + + + + + + Data Encryption Key + + + Data Encryption keys are 128, 192, or 256-bit length randomly generate keys. + + + + + MAC Key + + + MAC key is a 512-bit randomly generated key. + SHA512 is the algorithm used along with the + MAC key to compute a cryptographic hash for + integrity purposes. + + + + + + + + The data key wrapping algorithm is as follows: + + + + Generate random IV. + + + Add padding to the plain text following PKCS#7 described in + RFC 2315. + + + Encrypt padded plain text with the IV + using AES256 in CBC + mode. + + + Compute HMAC over the encrypted data. + + + Concatenate HMAC, IV + and encrypted cipher text. + + + + + diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml new file mode 100644 index 38e8aa0..b96f4ac *** a/doc/src/sgml/filelist.sgml --- b/doc/src/sgml/filelist.sgml *************** *** 49,54 **** --- 49,55 ---- + diff --git a/doc/src/sgml/installation.sgml b/doc/src/sgml/installation.sgml new file mode 100644 index 0ac1cb9..bcc8024 *** a/doc/src/sgml/installation.sgml --- b/doc/src/sgml/installation.sgml *************** build-postgresql: *** 976,983 **** Build with support for SSL (encrypted) ! connections. This requires the OpenSSL ! package to be installed. configure will check for the required header files and libraries to make sure that your OpenSSL installation is sufficient before proceeding. --- 976,984 ---- Build with support for SSL (encrypted) ! connections and cluster file encryption. This requires the ! OpenSSL package to be installed. ! configure will check for the required header files and libraries to make sure that your OpenSSL installation is sufficient before proceeding. diff --git a/doc/src/sgml/postgres.sgml b/doc/src/sgml/postgres.sgml new file mode 100644 index 730d5fd..0ea7da6 *** a/doc/src/sgml/postgres.sgml --- b/doc/src/sgml/postgres.sgml *************** break is not needed in a wider output re *** 171,176 **** --- 171,177 ---- &wal; &logical-replication; &jit; + &database-encryption; ®ress; diff --git a/doc/src/sgml/ref/initdb.sgml b/doc/src/sgml/ref/initdb.sgml new file mode 100644 index 385ac25..9aa8888 *** a/doc/src/sgml/ref/initdb.sgml --- b/doc/src/sgml/ref/initdb.sgml *************** PostgreSQL documentation *** 163,168 **** --- 163,198 ---- + + + + + This option specifies an external command to obtain the passphrase + for cluster file encryption during server initialization and + server start. + + + The command can prompt for the passphrase or PIN from the terminal + if is used, and must exit with + code 0. In the parameter value, %R represents + the file descriptor number opened to the terminal that started + the server. This is useful for prompting from the terminal. + A file descriptor is only available if enabled at server start. If + %R is used and no file descriptor is available, + the server will not start. Value %P is replaced + by a pre-defined prompt string. Value %d is + replaced by the directory containing the keys; this is useful + if the command must create files with the keys, e.g., to store a + passphrase encryped by a key stored in a hardware security module. + (Write %% for a literal %.) + Note that the prompt string will probably contain whitespace, so + be sure to quote its use adequately. A single newline is stripped + from the end of the output if present. The output passphrase must + be at least 64 bytes. + + + + *************** PostgreSQL documentation *** 223,228 **** --- 253,270 ---- + + + + + + Specifies the number of bits for the file encryption keys. The + default is 128 bits. + + + + *************** PostgreSQL documentation *** 286,291 **** --- 328,344 ---- + + + + + Allows the command + to prompt for a passphrase or PIN. + + + + + *************** PostgreSQL documentation *** 306,311 **** --- 359,376 ---- + + + + + + + Copies cluster file encryption keys from another cluster; required + when using pg_upgrade on a cluster + with cluster file encryption enabled. + + + diff --git a/doc/src/sgml/ref/pg_altercpass.sgml b/doc/src/sgml/ref/pg_altercpass.sgml new file mode 100644 index ...1683d08 *** a/doc/src/sgml/ref/pg_altercpass.sgml --- b/doc/src/sgml/ref/pg_altercpass.sgml *************** *** 0 **** --- 1,186 ---- + + + + + pg_altercpass + + + + pg_altercpass + 1 + Application + + + + pg_altercpass + alter the PostgreSQL cluster passphrase + + + + + pg_altercpass + + + + + old_cluster_passphrase_command + new_cluster_passphrase_command + + + + + + datadir + + + + + pg_altercpass + + + + + + + + + + + + + + datadir + + + + + + Description + + pg_altercpass alters the cluster passphrase used + for cluster file encryption. The cluster passphrase is initially set + during . The command can be run while the + server is running or stopped. The new password must be used the next + time the server is started. + + + + Technically, pg_altercpass changes the key + encryption key (KEK) which encrypts the data + encryption keys; it does not change the data encryption keys. It does + this by decrypting each data encryption key using the old_cluster_passphrase_command, + re-encrypting it using the new_cluster_passphrase_command, and + then writes the result back to the cluster directory. + + + + See the documentation for how to define + the old and new passprhase commands. You can use different executables + for these commands, or you can use the same executable with different + arguments to specify retrieval of the old or new key. + + + + When started, pg_altercpass repairs any files that + remain from previous pg_altercpass failures before + altering the cluster passphrase. To perform only the repair task, + use the option. The server will not start + if repair is needed, though a running server is unaffected by an + unrepaired cluster passphrase configuration. + + + + You can specify the data directory on the command line, or use + the environment variable PGDATA. + + + + + Options + + + + + + + + Allows the and + commands + to prompt for a passphrase or PIN. + + + + + + + + Other options: + + + + + + + + Print the pg_altercpass version and exit. + + + + + + + + + + Show help about pg_altercpass command line + arguments, and exit. + + + + + + + + + + + Environment + + + + PGDATA + + + + Default data directory location + + + + + + PG_COLOR + + + Specifies whether to use color in diagnostic messages. Possible values + are always, auto and + never. + + + + + + + + See Also + + + + + + + diff --git a/doc/src/sgml/ref/pg_ctl-ref.sgml b/doc/src/sgml/ref/pg_ctl-ref.sgml new file mode 100644 index 3946fa5..a2f12a4 *** a/doc/src/sgml/ref/pg_ctl-ref.sgml --- b/doc/src/sgml/ref/pg_ctl-ref.sgml *************** PostgreSQL documentation *** 38,43 **** --- 38,44 ---- options path + *************** PostgreSQL documentation *** 72,77 **** --- 73,79 ---- seconds options + *************** PostgreSQL documentation *** 372,377 **** --- 374,391 ---- + + + + + + + Allows the command + to prompt for a passphrase or PIN. + reporting. + + + diff --git a/doc/src/sgml/ref/pgupgrade.sgml b/doc/src/sgml/ref/pgupgrade.sgml new file mode 100644 index 92e1d09..5d7a8b3 *** a/doc/src/sgml/ref/pgupgrade.sgml --- b/doc/src/sgml/ref/pgupgrade.sgml *************** PostgreSQL documentation *** 168,173 **** --- 168,180 ---- + + + allows prompting for a passphrase or PIN + + + + dir dir directory to use for postmaster sockets during upgrade; *************** make prefix=/usr/local/pgsql.new install *** 309,315 **** Again, use compatible initdb flags that match the old cluster. Many prebuilt installers do this step automatically. There is no need to ! start the new cluster. --- 316,324 ---- Again, use compatible initdb flags that match the old cluster. Many prebuilt installers do this step automatically. There is no need to ! start the new cluster. If upgrading a cluster that uses ! cluster file encryption, the initdb option ! must be specified. *************** psql --username=postgres --file=script.s *** 838,843 **** --- 847,859 ---- is down. + + If the old cluster uses file encryption, the new cluster must use + the same keys, so pg_upgrade copies them to the + new cluster. It is necessary to initialize the new cluster with + the same cluster_passphrase_command and the same + file encryption key length. + diff --git a/doc/src/sgml/ref/postgres-ref.sgml b/doc/src/sgml/ref/postgres-ref.sgml new file mode 100644 index 4aaa7ab..a996dec *** a/doc/src/sgml/ref/postgres-ref.sgml --- b/doc/src/sgml/ref/postgres-ref.sgml *************** PostgreSQL documentation *** 298,303 **** --- 298,316 ---- + + + + Makes postgres prompt for the cluster passphrase + from the specified open numeric file descriptor. The descriptor + is closed after the key is read. The file descriptor number + -1 duplicates standard error for the terminal; + this is useful for single-user mode. + + + + + diff --git a/doc/src/sgml/storage.sgml b/doc/src/sgml/storage.sgml new file mode 100644 index 3234adb..cdbc214 *** a/doc/src/sgml/storage.sgml --- b/doc/src/sgml/storage.sgml *************** Item *** 78,83 **** --- 78,88 ---- + pg_cryptokeys + Subdirectory containing file encryption keys + + + pg_dynshmem Subdirectory containing files used by the dynamic shared memory subsystem diff --git a/src/backend/Makefile b/src/backend/Makefile new file mode 100644 index 9706a95..4ace302 *** a/src/backend/Makefile --- b/src/backend/Makefile *************** SUBDIRS = access bootstrap catalog parse *** 21,27 **** main nodes optimizer partitioning port postmaster \ regex replication rewrite \ statistics storage tcop tsearch utils $(top_builddir)/src/timezone \ ! jit include $(srcdir)/common.mk --- 21,27 ---- main nodes optimizer partitioning port postmaster \ regex replication rewrite \ statistics storage tcop tsearch utils $(top_builddir)/src/timezone \ ! jit crypto include $(srcdir)/common.mk diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c new file mode 100644 index 8dd225c..31fe655 *** a/src/backend/access/transam/xlog.c --- b/src/backend/access/transam/xlog.c *************** *** 44,54 **** --- 44,56 ---- #include "commands/tablespace.h" #include "common/controldata_utils.h" #include "executor/instrument.h" + #include "crypto/kmgr.h" #include "miscadmin.h" #include "pg_trace.h" #include "pgstat.h" #include "port/atomics.h" #include "postmaster/bgwriter.h" + #include "postmaster/postmaster.h" #include "postmaster/startup.h" #include "postmaster/walwriter.h" #include "replication/basebackup.h" *************** *** 81,86 **** --- 83,89 ---- #include "utils/timestamp.h" extern uint32 bootstrap_data_checksum_version; + extern int bootstrap_file_encryption_keylen; /* Unsupported old recovery command file names (relative to $PGDATA) */ #define RECOVERY_COMMAND_FILE "recovery.conf" *************** InitControlFile(uint64 sysidentifier) *** 4608,4613 **** --- 4611,4617 ---- ControlFile->wal_log_hints = wal_log_hints; ControlFile->track_commit_timestamp = track_commit_timestamp; ControlFile->data_checksum_version = bootstrap_data_checksum_version; + ControlFile->file_encryption_keylen = bootstrap_file_encryption_keylen; } static void *************** ReadControlFile(void) *** 4707,4712 **** --- 4711,4717 ---- pg_crc32c crc; int fd; static char wal_segsz_str[20]; + static char file_encryption_keylen_str[20]; int r; /* *************** ReadControlFile(void) *** 4895,4900 **** --- 4900,4911 ---- /* Make the initdb settings visible as GUC variables, too */ SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no", PGC_INTERNAL, PGC_S_OVERRIDE); + + Assert(ControlFile != NULL); + snprintf(file_encryption_keylen_str, sizeof(file_encryption_keylen_str), "%d", + ControlFile->file_encryption_keylen); + SetConfigOption("file_encryption_keylen", file_encryption_keylen_str, PGC_INTERNAL, + PGC_S_OVERRIDE); } /* *************** BootStrapXLOG(void) *** 5343,5348 **** --- 5354,5368 ---- /* some additional ControlFile fields are set in WriteControlFile() */ WriteControlFile(); + /* Enable file encryption if required */ + if (ControlFile->file_encryption_keylen > 0) + BootStrapKmgr(); + if (terminal_fd != -1) + { + close(terminal_fd); + terminal_fd = -1; + } + /* Bootstrap the commit log, too */ BootStrapCLOG(); BootStrapCommitTs(); diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c new file mode 100644 index a7ed93f..bf93135 *** a/src/backend/bootstrap/bootstrap.c --- b/src/backend/bootstrap/bootstrap.c *************** *** 28,39 **** --- 28,41 ---- #include "catalog/pg_collation.h" #include "catalog/pg_type.h" #include "common/link-canary.h" + #include "crypto/kmgr.h" #include "libpq/pqsignal.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "pg_getopt.h" #include "pgstat.h" #include "postmaster/bgwriter.h" + #include "postmaster/postmaster.h" #include "postmaster/startup.h" #include "postmaster/walwriter.h" #include "replication/walreceiver.h" *************** *** 51,56 **** --- 53,60 ---- #include "utils/relmapper.h" uint32 bootstrap_data_checksum_version = 0; /* No checksum */ + int bootstrap_file_encryption_keylen = 0; /* disabled */ + char *bootstrap_old_key_datadir = NULL; /* disabled */ static void CheckerModeMain(void); *************** AuxiliaryProcessMain(int argc, char *arg *** 224,230 **** /* If no -x argument, we are a CheckerProcess */ MyAuxProcType = CheckerProcess; ! while ((flag = getopt(argc, argv, "B:c:d:D:Fkr:x:X:-:")) != -1) { switch (flag) { --- 228,234 ---- /* If no -x argument, we are a CheckerProcess */ MyAuxProcType = CheckerProcess; ! while ((flag = getopt(argc, argv, "B:c:d:D:FkK:r:R:u:x:X:-:")) != -1) { switch (flag) { *************** AuxiliaryProcessMain(int argc, char *arg *** 253,261 **** --- 257,274 ---- case 'k': bootstrap_data_checksum_version = PG_DATA_CHECKSUM_VERSION; break; + case 'K': + bootstrap_file_encryption_keylen = atoi(optarg); + break; + case 'u': + bootstrap_old_key_datadir = pstrdup(optarg); + break; case 'r': strlcpy(OutputFileName, optarg, MAXPGPATH); break; + case 'R': + terminal_fd = atoi(optarg); + break; case 'x': MyAuxProcType = atoi(optarg); break; *************** AuxiliaryProcessMain(int argc, char *arg *** 312,317 **** --- 325,336 ---- proc_exit(1); } + if (bootstrap_file_encryption_keylen != 0 && + bootstrap_file_encryption_keylen != 128 && + bootstrap_file_encryption_keylen != 192 && + bootstrap_file_encryption_keylen != 256) + elog(PANIC, "unrecognized file encryption length: %d", bootstrap_file_encryption_keylen); + switch (MyAuxProcType) { case StartupProcess: diff --git a/src/backend/crypto/Makefile b/src/backend/crypto/Makefile new file mode 100644 index ...c273620 *** a/src/backend/crypto/Makefile --- b/src/backend/crypto/Makefile *************** *** 0 **** --- 1,18 ---- + #------------------------------------------------------------------------- + # + # Makefile + # Makefile for src/backend/crypto + # + # IDENTIFICATION + # src/backend/crypto/Makefile + # + #------------------------------------------------------------------------- + + subdir = src/backend/crypto + top_builddir = ../../.. + include $(top_builddir)/src/Makefile.global + + OBJS = \ + kmgr.o + + include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/crypto/kmgr.c b/src/backend/crypto/kmgr.c new file mode 100644 index ...24c5d7f *** a/src/backend/crypto/kmgr.c --- b/src/backend/crypto/kmgr.c *************** *** 0 **** --- 1,359 ---- + /*------------------------------------------------------------------------- + * + * kmgr.c + * Cluster file encryption routines + * + * Cluster file encryption is enabled if user requests it during initdb. + * During bootstrap, we generate data encryption keys, wrap them with + * KEK which is derived from the user-provided passphrase, and store + * them into each file located at KMGR_DIR. Once generated, these are + * not changed. During startup, we decrypt all internal keys and load + * them to the shared memory space. Internal keys on the shared memory + * are read-only. All wrapping and unwrapping key routines depend on + * the OpenSSL library for now. + * + * Copyright (c) 2020, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/crypto/kmgr.c + *------------------------------------------------------------------------- + */ + + #include "postgres.h" + + #include + #include + + #include "funcapi.h" + #include "miscadmin.h" + #include "pgstat.h" + + #include "common/file_perm.h" + #include "common/kmgr_utils.h" + #include "common/sha2.h" + #include "access/xlog.h" + #include "crypto/kmgr.h" + #include "storage/copydir.h" + #include "storage/fd.h" + #include "storage/ipc.h" + #include "storage/shmem.h" + #include "utils/builtins.h" + #include "utils/memutils.h" + /* Struct stores file encryption keys in plaintext format */ + typedef struct KmgrShmemData + { + CryptoKey intlKeys[KMGR_MAX_INTERNAL_KEYS]; + } KmgrShmemData; + static KmgrShmemData *KmgrShmem; + + /* GUC variables */ + char *cluster_passphrase_command = NULL; + int file_encryption_keylen = 0; + + CryptoKey bootstrap_keys[KMGR_MAX_INTERNAL_KEYS]; + + extern char *bootstrap_old_key_datadir; + extern int bootstrap_file_encryption_keylen; + + static void bzeroKmgrKeys(int status, Datum arg); + static void KmgrSaveCryptoKeys(const char *dir, CryptoKey *keys); + static CryptoKey *generate_crypto_key(int len); + + /* + * This function must be called ONCE during initdb. + */ + void + BootStrapKmgr(void) + { + char live_path[MAXPGPATH]; + CryptoKey *keys_wrap; + int nkeys; + char passphrase[KMGR_MAX_PASSPHRASE_LEN]; + int passlen; + + #ifndef USE_OPENSSL + ereport(ERROR, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + (errmsg("cluster encryption is not supported because OpenSSL is not supported by this build"), + errhint("Compile with --with-openssl to use cluster encryption.")))); + #endif + + snprintf(live_path, sizeof(live_path), "%s/%s", DataDir, LIVE_KMGR_DIR); + + /* copy cluster file encryption keys from an old cluster? */ + if (bootstrap_old_key_datadir != NULL) + { + char old_key_dir[MAXPGPATH]; + + snprintf(old_key_dir, sizeof(old_key_dir), "%s/%s", + bootstrap_old_key_datadir, LIVE_KMGR_DIR); + copydir(old_key_dir, LIVE_KMGR_DIR, true); + } + /* create empty directory */ + else + { + if (mkdir(LIVE_KMGR_DIR, pg_dir_create_mode) < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not create cluster file encryption directory \"%s\": %m", + LIVE_KMGR_DIR))); + } + + /* + * Get key encryption key from the passphrase command. The passphrase + * command might want to check for the existance of files in the + * live directory, so run this _after_ copying the directory in place. + */ + passlen = kmgr_run_cluster_passphrase_command(cluster_passphrase_command, + passphrase, KMGR_MAX_PASSPHRASE_LEN, + live_path); + if (passlen < KMGR_MIN_PASSPHRASE_LEN) + ereport(ERROR, + (errmsg("passphrase must be at least %d bytes", + KMGR_MIN_PASSPHRASE_LEN))); + + /* generate new cluster file encryption keys */ + if (bootstrap_old_key_datadir == NULL) + { + CryptoKey bootstrap_keys_wrap[KMGR_MAX_INTERNAL_KEYS]; + PgKeyWrapCtx *ctx; + uint8 KEK_enc[KMGR_ENC_KEY_LEN]; + uint8 KEK_hmac[KMGR_MAC_KEY_LEN]; + + /* Get key encryption key and HMAC key from passphrase */ + kmgr_derive_keys(passphrase, passlen, KEK_enc, KEK_hmac); + + /* Create temporary key wrap context */ + ctx = pg_create_keywrap_ctx(KEK_enc, KEK_hmac); + if (!ctx) + elog(ERROR, "could not initialize encryption context"); + + /* Wrap all data encryption keys by key encryption key */ + for (int id = 0; id < KMGR_MAX_INTERNAL_KEYS; id++) + { + CryptoKey *key; + + /* generate a data encryption key */ + key = generate_crypto_key(bootstrap_file_encryption_keylen); + + if (!kmgr_wrap_key(ctx, key, &(bootstrap_keys_wrap[id]))) + { + pg_free_keywrap_ctx(ctx); + elog(ERROR, "failed to wrap data encryption key"); + } + + explicit_bzero(key, sizeof(CryptoKey)); + } + + /* Save data encryption keys to the disk */ + KmgrSaveCryptoKeys(LIVE_KMGR_DIR, bootstrap_keys_wrap); + + explicit_bzero(bootstrap_keys_wrap, sizeof(bootstrap_keys_wrap)); + pg_free_keywrap_ctx(ctx); + } + + /* + * We are either decrypting keys we copied from an old cluster, or + * decrypting keys we just wrote above --- either way, we decrypt + * them here and store them in a file-scoped variable for use in + * later encrypting during bootstrap mode. + */ + /* Get the crypto keys from the file */ + keys_wrap = kmgr_get_cryptokeys(LIVE_KMGR_DIR, &nkeys); + Assert(nkeys == KMGR_MAX_INTERNAL_KEYS); + + if (!kmgr_verify_passphrase(passphrase, passlen, keys_wrap, bootstrap_keys, + KMGR_MAX_INTERNAL_KEYS)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cluster passphrase does not match expected passphrase"))); + + /* bzero keys on exit */ + on_proc_exit(bzeroKmgrKeys, 0); + + explicit_bzero(passphrase, passlen); + } + + /* Report shared-memory space needed by KmgrShmem */ + Size + KmgrShmemSize(void) + { + if (!file_encryption_keylen) + return 0; + + return MAXALIGN(sizeof(KmgrShmemData)); + } + + /* Allocate and initialize key manager memory */ + void + KmgrShmemInit(void) + { + bool found; + + if (!file_encryption_keylen) + return; + + KmgrShmem = (KmgrShmemData *) ShmemInitStruct("File encryption key manager", + KmgrShmemSize(), &found); + + on_shmem_exit(bzeroKmgrKeys, 0); + } + + /* + * Get encryption key passphrase and verify it, then get the data encryption keys. + * This function is called by postmaster at startup time. + */ + void + InitializeKmgr(void) + { + CryptoKey *keys_wrap; + int nkeys; + char passphrase[KMGR_MAX_PASSPHRASE_LEN]; + int passlen; + struct stat buffer; + char live_path[MAXPGPATH]; + + if (!file_encryption_keylen) + return; + + elog(DEBUG1, "starting up cluster file encryption manager"); + + if (stat(KMGR_DIR, &buffer) != 0 || !S_ISDIR(buffer.st_mode)) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + (errmsg("cluster file encryption directory %s is missing", KMGR_DIR)))); + + if (stat(KMGR_DIR_PID, &buffer) == 0) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + (errmsg("cluster had a pg_altercpass failure that needs repair or pg_altercpass is running"), + errhint("Run pg_altercpass --repair or wait for it to complete.")))); + + /* + * We want OLD deleted since it allows access to the data encryption + * keys using the old cluster passphrase. If NEW exists, it means either + * NEW is partly written, or NEW wasn't renamed to LIVE --- in either + * case, it needs to be repaired. + */ + if (stat(OLD_KMGR_DIR, &buffer) == 0 || stat(NEW_KMGR_DIR, &buffer) == 0) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + (errmsg("cluster had a pg_altercpass failure that needs repair"), + errhint("Run pg_altercpass --repair.")))); + + /* If OLD, NEW, and LIVE do not exist, there is a serious problem. */ + if (stat(LIVE_KMGR_DIR, &buffer) != 0 || !S_ISDIR(buffer.st_mode)) + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + (errmsg("cluster has no data encryption keys")))); + + /* Get cluster passphrase */ + snprintf(live_path, sizeof(live_path), "%s/%s", DataDir, LIVE_KMGR_DIR); + passlen = kmgr_run_cluster_passphrase_command(cluster_passphrase_command, + passphrase, KMGR_MAX_PASSPHRASE_LEN, + live_path); + + /* Get the crypto keys from the file */ + keys_wrap = kmgr_get_cryptokeys(LIVE_KMGR_DIR, &nkeys); + Assert(nkeys == KMGR_MAX_INTERNAL_KEYS); + + /* + * Verify passphrase and prepare a data encryption key in plaintext in shared memory. + */ + if (!kmgr_verify_passphrase(passphrase, passlen, keys_wrap, KmgrShmem->intlKeys, + KMGR_MAX_INTERNAL_KEYS)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cluster passphrase does not match expected passphrase"))); + + explicit_bzero(passphrase, passlen); + } + + static void + bzeroKmgrKeys(int status, Datum arg) + { + if (IsBootstrapProcessingMode()) + explicit_bzero(bootstrap_keys, sizeof(bootstrap_keys)); + else + explicit_bzero(KmgrShmem->intlKeys, sizeof(KmgrShmem->intlKeys)); + } + + const CryptoKey * + KmgrGetKey(int id) + { + Assert(id < KMGR_MAX_INTERNAL_KEYS); + + return (const CryptoKey *) (IsBootstrapProcessingMode() ? + &(bootstrap_keys[id]) : &(KmgrShmem->intlKeys[id])); + } + + /* Generate an empty CryptoKey */ + static CryptoKey * + generate_crypto_key(int len) + { + CryptoKey *newkey; + + Assert(len < KMGR_MAX_KEY_LEN); + newkey = (CryptoKey *) palloc0(sizeof(CryptoKey)); + + if (!pg_strong_random(newkey->key, len)) + elog(ERROR, "failed to generate new file encryption key"); + + newkey->klen = len; + + return newkey; + } + + /* + * Save the given file encryption keys to the disk. We don't need CRC + * check for crypto keys because these keys have HMAC which is used + * for integrity check during unwrapping. + */ + static void + KmgrSaveCryptoKeys(const char *dir, CryptoKey *keys) + { + elog(DEBUG2, "saving all cryptographic keys"); + + for (int i = 0; i < KMGR_MAX_INTERNAL_KEYS; i++) + { + int fd; + char path[MAXPGPATH]; + + CryptoKeyFilePath(path, dir, i); + + if ((fd = BasicOpenFile(path, O_RDWR | O_CREAT | O_EXCL | PG_BINARY)) < 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open file \"%s\": %m", + path))); + + errno = 0; + pgstat_report_wait_start(WAIT_EVENT_KEY_FILE_WRITE); + if (write(fd, &(keys[i]), sizeof(CryptoKey)) != sizeof(CryptoKey)) + { + /* if write didn't set errno, assume problem is no disk space */ + if (errno == 0) + errno = ENOSPC; + + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not write file \"%s\": %m", + path))); + } + pgstat_report_wait_end(); + + pgstat_report_wait_start(WAIT_EVENT_KEY_FILE_SYNC); + if (pg_fsync(fd) != 0) + ereport(PANIC, + (errcode_for_file_access(), + errmsg("could not fsync file \"%s\": %m", + path))); + pgstat_report_wait_end(); + + if (close(fd) != 0) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not close file \"%s\": %m", + path))); + } + } diff --git a/src/backend/main/main.c b/src/backend/main/main.c new file mode 100644 index b6e5128..65d191d *** a/src/backend/main/main.c --- b/src/backend/main/main.c *************** help(const char *progname) *** 324,329 **** --- 324,330 ---- #endif printf(_(" -N MAX-CONNECT maximum number of allowed connections\n")); printf(_(" -p PORT port number to listen on\n")); + printf(_(" -R fd prompt for the cluster passphrase\n")); printf(_(" -s show statistics after each query\n")); printf(_(" -S WORK-MEM set amount of memory for sorts (in kB)\n")); printf(_(" -V, --version output version information, then exit\n")); *************** help(const char *progname) *** 351,357 **** --- 352,360 ---- printf(_("\nOptions for bootstrapping mode:\n")); printf(_(" --boot selects bootstrapping mode (must be first argument)\n")); printf(_(" DBNAME database name (mandatory argument in bootstrapping mode)\n")); + printf(_(" -K LEN enable cluster file encryption with specified key length\n")); printf(_(" -r FILENAME send stdout and stderr to given file\n")); + printf(_(" -u DATADIR copy encryption keys from datadir\n")); printf(_(" -x NUM internal use\n")); printf(_("\nPlease read the documentation for the complete list of run-time\n" diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c new file mode 100644 index 7c75a25..7459287 *** a/src/backend/postmaster/pgstat.c --- b/src/backend/postmaster/pgstat.c *************** pgstat_get_wait_io(WaitEventIO w) *** 4155,4160 **** --- 4155,4169 ---- case WAIT_EVENT_DSM_FILL_ZERO_WRITE: event_name = "DSMFillZeroWrite"; break; + case WAIT_EVENT_KEY_FILE_READ: + event_name = "KeyFileRead"; + break; + case WAIT_EVENT_KEY_FILE_WRITE: + event_name = "KeyFileWrite"; + break; + case WAIT_EVENT_KEY_FILE_SYNC: + event_name = "KeyFileSync"; + break; case WAIT_EVENT_LOCK_FILE_ADDTODATADIR_READ: event_name = "LockFileAddToDataDirRead"; break; diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c new file mode 100644 index 5d09822..297b3fc *** a/src/backend/postmaster/postmaster.c --- b/src/backend/postmaster/postmaster.c *************** *** 100,105 **** --- 100,106 ---- #include "common/file_perm.h" #include "common/ip.h" #include "common/string.h" + #include "crypto/kmgr.h" #include "lib/ilist.h" #include "libpq/auth.h" #include "libpq/libpq.h" *************** static int SendStop = false; *** 230,235 **** --- 231,237 ---- /* still more option variables */ bool EnableSSL = false; + int terminal_fd = -1; int PreAuthDelay = 0; int AuthenticationTimeout = 60; *************** PostmasterMain(int argc, char *argv[]) *** 686,692 **** * tcop/postgres.c (the option sets should not conflict) and with the * common help() function in main/main.c. */ ! while ((opt = getopt(argc, argv, "B:bc:C:D:d:EeFf:h:ijk:lN:nOPp:r:S:sTt:W:-:")) != -1) { switch (opt) { --- 688,694 ---- * tcop/postgres.c (the option sets should not conflict) and with the * common help() function in main/main.c. */ ! while ((opt = getopt(argc, argv, "B:bc:C:D:d:EeFf:h:ijk:lN:nOPp:r:R:S:sTt:W:-:")) != -1) { switch (opt) { *************** PostmasterMain(int argc, char *argv[]) *** 777,782 **** --- 779,788 ---- /* only used by single-user backend */ break; + case 'R': + terminal_fd = atoi(optarg); + break; + case 'S': SetConfigOption("work_mem", optarg, PGC_POSTMASTER, PGC_S_ARGV); break; *************** PostmasterMain(int argc, char *argv[]) *** 1325,1330 **** --- 1331,1340 ---- */ RemovePgTempFiles(); + InitializeKmgr(); + if (terminal_fd != -1) + close(terminal_fd); + /* * Initialize stats collection subsystem (this does NOT start the * collector process!) diff --git a/src/backend/replication/basebackup.c b/src/backend/replication/basebackup.c new file mode 100644 index 1d8d174..c9fc2f4 *** a/src/backend/replication/basebackup.c --- b/src/backend/replication/basebackup.c *************** *** 18,23 **** --- 18,24 ---- #include "access/xlog_internal.h" /* for pg_start/stop_backup */ #include "catalog/pg_type.h" + #include "common/kmgr_utils.h" #include "common/file_perm.h" #include "commands/progress.h" #include "lib/stringinfo.h" *************** struct exclude_list_item *** 152,157 **** --- 153,162 ---- */ static const char *const excludeDirContents[] = { + /* Skip temporary crypto key directories */ + NEW_KMGR_DIR, + OLD_KMGR_DIR, + /* * Skip temporary statistics files. PG_STAT_TMP_DIR must be skipped even * when stats_temp_directory is set because PGSS_TEXT_FILE is always diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c new file mode 100644 index 96c2aaa..64fe10c *** a/src/backend/storage/ipc/ipci.c --- b/src/backend/storage/ipc/ipci.c *************** *** 23,28 **** --- 23,29 ---- #include "access/syncscan.h" #include "access/twophase.h" #include "commands/async.h" + #include "crypto/kmgr.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/autovacuum.h" *************** CreateSharedMemoryAndSemaphores(void) *** 149,154 **** --- 150,156 ---- size = add_size(size, BTreeShmemSize()); size = add_size(size, SyncScanShmemSize()); size = add_size(size, AsyncShmemSize()); + size = add_size(size, KmgrShmemSize()); #ifdef EXEC_BACKEND size = add_size(size, ShmemBackendArraySize()); #endif *************** CreateSharedMemoryAndSemaphores(void) *** 267,272 **** --- 269,275 ---- BTreeShmemInit(); SyncScanShmemInit(); AsyncShmemInit(); + KmgrShmemInit(); #ifdef EXEC_BACKEND diff --git a/src/backend/storage/lmgr/lwlocknames.txt b/src/backend/storage/lmgr/lwlocknames.txt new file mode 100644 index 774292f..a44805d *** a/src/backend/storage/lmgr/lwlocknames.txt --- b/src/backend/storage/lmgr/lwlocknames.txt *************** XactTruncationLock 44 *** 53,55 **** --- 53,56 ---- # 45 was XactTruncationLock until removal of BackendRandomLock WrapLimitsVacuumLock 46 NotifyQueueTailLock 47 + KmgrFileLock 48 diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c new file mode 100644 index 3679799..0c1d038 *** a/src/backend/tcop/postgres.c --- b/src/backend/tcop/postgres.c *************** *** 42,47 **** --- 42,48 ---- #include "catalog/pg_type.h" #include "commands/async.h" #include "commands/prepare.h" + #include "crypto/kmgr.h" #include "executor/spi.h" #include "jit/jit.h" #include "libpq/libpq.h" *************** process_postgres_switches(int argc, char *** 3559,3565 **** * postmaster/postmaster.c (the option sets should not conflict) and with * the common help() function in main/main.c. */ ! while ((flag = getopt(argc, argv, "B:bc:C:D:d:EeFf:h:ijk:lN:nOPp:r:S:sTt:v:W:-:")) != -1) { switch (flag) { --- 3560,3566 ---- * postmaster/postmaster.c (the option sets should not conflict) and with * the common help() function in main/main.c. */ ! while ((flag = getopt(argc, argv, "B:bc:C:D:d:EeFf:h:ijk:lN:nOPp:r:R:S:sTt:v:W:-:")) != -1) { switch (flag) { *************** process_postgres_switches(int argc, char *** 3651,3656 **** --- 3652,3667 ---- strlcpy(OutputFileName, optarg, MAXPGPATH); break; + case 'R': + terminal_fd = atoi(optarg); + if (terminal_fd == -1) + /* + * Allow file descriptor closing to be bypassed via -1. + * We just dup sterr. This is useful for single-user mode. + */ + terminal_fd = dup(2); + break; + case 'S': SetConfigOption("work_mem", optarg, ctx, gucsource); break; *************** PostgresMain(int argc, char *argv[], *** 3903,3908 **** --- 3914,3930 ---- BaseInit(); /* + * Initialize kmgr for cluster encryption. Since kmgr needs to attach to + * shared memory the initialization must be called after BaseInit(). + */ + if (!IsUnderPostmaster) + { + InitializeKmgr(); + if (terminal_fd != -1) + close(terminal_fd); + } + + /* * Create a per-backend PGPROC struct in shared memory, except in the * EXEC_BACKEND case where this was done in SubPostmasterMain. We must do * this before we can use LWLocks (and in the EXEC_BACKEND case we already diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c new file mode 100644 index dabcbb0..d889ca5 *** a/src/backend/utils/misc/guc.c --- b/src/backend/utils/misc/guc.c *************** *** 47,52 **** --- 47,53 ---- #include "commands/vacuum.h" #include "commands/variable.h" #include "common/string.h" + #include "crypto/kmgr.h" #include "funcapi.h" #include "jit/jit.h" #include "libpq/auth.h" *************** const char *const config_group_names[] = *** 745,750 **** --- 746,753 ---- gettext_noop("Statistics / Monitoring"), /* STATS_COLLECTOR */ gettext_noop("Statistics / Query and Index Statistics Collector"), + /* ENCRYPTION */ + gettext_noop("Encryption"), /* AUTOVACUUM */ gettext_noop("Autovacuum"), /* CLIENT_CONN */ *************** static struct config_int ConfigureNamesI *** 3389,3394 **** --- 3392,3408 ---- check_huge_page_size, NULL, NULL }, + { + {"file_encryption_keylen", PGC_INTERNAL, PRESET_OPTIONS, + gettext_noop("Shows the bit length of the file encryption key."), + NULL, + GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE + }, + &file_encryption_keylen, + 0, 0, 256, + NULL, NULL, NULL + }, + /* End-of-list marker */ { {NULL, 0, 0, NULL, NULL}, NULL, 0, 0, 0, NULL, NULL, NULL *************** static struct config_string ConfigureNam *** 4382,4387 **** --- 4396,4411 ---- "", NULL, NULL, NULL }, + + { + {"cluster_passphrase_command", PGC_SIGHUP, ENCRYPTION, + gettext_noop("Command to obtain passphrase for cluster file encryption."), + NULL + }, + &cluster_passphrase_command, + "", + NULL, NULL, NULL + }, { {"application_name", PGC_USERSET, LOGGING_WHAT, diff --git a/src/backend/utils/misc/pg_controldata.c b/src/backend/utils/misc/pg_controldata.c new file mode 100644 index d50d87a..6fcab31 *** a/src/backend/utils/misc/pg_controldata.c --- b/src/backend/utils/misc/pg_controldata.c *************** pg_control_recovery(PG_FUNCTION_ARGS) *** 263,270 **** Datum pg_control_init(PG_FUNCTION_ARGS) { ! Datum values[11]; ! bool nulls[11]; TupleDesc tupdesc; HeapTuple htup; ControlFileData *ControlFile; --- 263,270 ---- Datum pg_control_init(PG_FUNCTION_ARGS) { ! Datum values[12]; ! bool nulls[12]; TupleDesc tupdesc; HeapTuple htup; ControlFileData *ControlFile; *************** pg_control_init(PG_FUNCTION_ARGS) *** 274,280 **** * Construct a tuple descriptor for the result row. This must match this * function's pg_proc entry! */ ! tupdesc = CreateTemplateTupleDesc(11); TupleDescInitEntry(tupdesc, (AttrNumber) 1, "max_data_alignment", INT4OID, -1, 0); TupleDescInitEntry(tupdesc, (AttrNumber) 2, "database_block_size", --- 274,280 ---- * Construct a tuple descriptor for the result row. This must match this * function's pg_proc entry! */ ! tupdesc = CreateTemplateTupleDesc(12); TupleDescInitEntry(tupdesc, (AttrNumber) 1, "max_data_alignment", INT4OID, -1, 0); TupleDescInitEntry(tupdesc, (AttrNumber) 2, "database_block_size", *************** pg_control_init(PG_FUNCTION_ARGS) *** 297,302 **** --- 297,304 ---- BOOLOID, -1, 0); TupleDescInitEntry(tupdesc, (AttrNumber) 11, "data_page_checksum_version", INT4OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 12, "file_encryption_keylen", + INT4OID, -1, 0); tupdesc = BlessTupleDesc(tupdesc); /* read the control file */ *************** pg_control_init(PG_FUNCTION_ARGS) *** 338,343 **** --- 340,348 ---- values[10] = Int32GetDatum(ControlFile->data_checksum_version); nulls[10] = false; + values[11] = Int32GetDatum(ControlFile->file_encryption_keylen); + nulls[11] = false; + htup = heap_form_tuple(tupdesc, values, nulls); PG_RETURN_DATUM(HeapTupleGetDatum(htup)); diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample new file mode 100644 index b7fb2ec..0c13d66 *** a/src/backend/utils/misc/postgresql.conf.sample --- b/src/backend/utils/misc/postgresql.conf.sample *************** *** 632,637 **** --- 632,642 ---- # autovacuum, -1 means use # vacuum_cost_limit + #------------------------------------------------------------------------------ + # ENCRYPTION + #------------------------------------------------------------------------------ + + #cluster_passphrase_command = '' #------------------------------------------------------------------------------ # CLIENT CONNECTION DEFAULTS diff --git a/src/bin/Makefile b/src/bin/Makefile new file mode 100644 index 8b87035..191fa6c *** a/src/bin/Makefile --- b/src/bin/Makefile *************** include $(top_builddir)/src/Makefile.glo *** 16,21 **** --- 16,22 ---- SUBDIRS = \ initdb \ pg_archivecleanup \ + pg_altercpass \ pg_basebackup \ pg_checksums \ pg_config \ diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c new file mode 100644 index f994c42..489e8ed *** a/src/bin/initdb/initdb.c --- b/src/bin/initdb/initdb.c *************** static bool debug = false; *** 141,151 **** --- 141,156 ---- static bool noclean = false; static bool do_sync = true; static bool sync_only = false; + static bool pass_terminal_fd = false; + static char *term_fd_opt = NULL; + static int file_encryption_keylen = 0; static bool show_setting = false; static bool data_checksums = false; static char *xlog_dir = NULL; static char *str_wal_segment_size_mb = NULL; static int wal_segment_size_mb; + static char *cluster_passphrase_cmd = NULL; + static char *old_key_datadir = NULL; /* internal vars */ *************** static const char *const subdirs[] = { *** 203,208 **** --- 208,214 ---- "global", "pg_wal/archive_status", "pg_commit_ts", + "pg_cryptokeys", "pg_dynshmem", "pg_notify", "pg_serial", *************** test_config_settings(void) *** 954,965 **** test_buffs = MIN_BUFS_FOR_CONNS(test_conns); snprintf(cmd, sizeof(cmd), ! "\"%s\" --boot -x0 %s " "-c max_connections=%d " "-c shared_buffers=%d " "-c dynamic_shared_memory_type=%s " "< \"%s\" > \"%s\" 2>&1", backend_exec, boot_options, test_conns, test_buffs, dynamic_shared_memory_type, DEVNULL, DEVNULL); --- 960,972 ---- test_buffs = MIN_BUFS_FOR_CONNS(test_conns); snprintf(cmd, sizeof(cmd), ! "\"%s\" --boot -x0 %s %s " "-c max_connections=%d " "-c shared_buffers=%d " "-c dynamic_shared_memory_type=%s " "< \"%s\" > \"%s\" 2>&1", backend_exec, boot_options, + term_fd_opt ? term_fd_opt : "", test_conns, test_buffs, dynamic_shared_memory_type, DEVNULL, DEVNULL); *************** test_config_settings(void) *** 990,1001 **** } snprintf(cmd, sizeof(cmd), ! "\"%s\" --boot -x0 %s " "-c max_connections=%d " "-c shared_buffers=%d " "-c dynamic_shared_memory_type=%s " "< \"%s\" > \"%s\" 2>&1", backend_exec, boot_options, n_connections, test_buffs, dynamic_shared_memory_type, DEVNULL, DEVNULL); --- 997,1009 ---- } snprintf(cmd, sizeof(cmd), ! "\"%s\" --boot -x0 %s %s " "-c max_connections=%d " "-c shared_buffers=%d " "-c dynamic_shared_memory_type=%s " "< \"%s\" > \"%s\" 2>&1", backend_exec, boot_options, + term_fd_opt ? term_fd_opt : "", n_connections, test_buffs, dynamic_shared_memory_type, DEVNULL, DEVNULL); *************** setup_config(void) *** 1185,1190 **** --- 1193,1205 ---- "password_encryption = md5"); } + if (cluster_passphrase_cmd) + { + snprintf(repltok, sizeof(repltok), "cluster_passphrase_command = '%s'", + escape_quotes(cluster_passphrase_cmd)); + conflines = replace_token(conflines, "#cluster_passphrase_command = ''", repltok); + } + /* * If group access has been enabled for the cluster then it makes sense to * ensure that the log files also allow group access. Otherwise a backup *************** bootstrap_template1(void) *** 1394,1406 **** /* Also ensure backend isn't confused by this environment var: */ unsetenv("PGCLIENTENCODING"); snprintf(cmd, sizeof(cmd), ! "\"%s\" --boot -x1 -X %u %s %s %s", backend_exec, wal_segment_size_mb * (1024 * 1024), data_checksums ? "-k" : "", boot_options, ! debug ? "-d 5" : ""); PG_CMD_OPEN; --- 1409,1430 ---- /* Also ensure backend isn't confused by this environment var: */ unsetenv("PGCLIENTENCODING"); + if (file_encryption_keylen != 0) + sprintf(buf, "%d", file_encryption_keylen); + else + buf[0] = '\0'; + snprintf(cmd, sizeof(cmd), ! "\"%s\" --boot -x1 -X %u %s %s %s %s %s %s %s %s", backend_exec, wal_segment_size_mb * (1024 * 1024), data_checksums ? "-k" : "", + cluster_passphrase_cmd ? "-K" : "", buf, + old_key_datadir ? "-u" : "", + old_key_datadir ? old_key_datadir : "", boot_options, ! debug ? "-d 5" : "", ! term_fd_opt ? term_fd_opt : ""); PG_CMD_OPEN; *************** usage(const char *progname) *** 2281,2299 **** " set default locale in the respective category for\n" " new databases (default taken from environment)\n")); printf(_(" --no-locale equivalent to --locale=C\n")); ! printf(_(" --pwfile=FILE read password for the new superuser from file\n")); printf(_(" -T, --text-search-config=CFG\n" " default text search configuration\n")); printf(_(" -U, --username=NAME database superuser name\n")); ! printf(_(" -W, --pwprompt prompt for a password for the new superuser\n")); printf(_(" -X, --waldir=WALDIR location for the write-ahead log directory\n")); printf(_(" --wal-segsize=SIZE size of WAL segments, in megabytes\n")); printf(_("\nLess commonly used options:\n")); printf(_(" -d, --debug generate lots of debugging output\n")); printf(_(" -k, --data-checksums use data page checksums\n")); printf(_(" -L DIRECTORY where to find the input files\n")); printf(_(" -n, --no-clean do not clean up after errors\n")); printf(_(" -N, --no-sync do not wait for changes to be written safely to disk\n")); printf(_(" -s, --show show internal settings\n")); printf(_(" -S, --sync-only only sync data directory\n")); printf(_("\nOther options:\n")); --- 2305,2329 ---- " set default locale in the respective category for\n" " new databases (default taken from environment)\n")); printf(_(" --no-locale equivalent to --locale=C\n")); ! printf(_(" --pwfile=FILE read the new superuser password from file\n")); printf(_(" -T, --text-search-config=CFG\n" " default text search configuration\n")); printf(_(" -U, --username=NAME database superuser name\n")); ! printf(_(" -W, --pwprompt prompt for the new superuser password\n")); printf(_(" -X, --waldir=WALDIR location for the write-ahead log directory\n")); printf(_(" --wal-segsize=SIZE size of WAL segments, in megabytes\n")); printf(_("\nLess commonly used options:\n")); + printf(_(" -c --cluster-passphrase-command=COMMAND\n" + " enable cluster file encryption and set command\n" + " to obtain the cluster passphrase\n")); printf(_(" -d, --debug generate lots of debugging output\n")); printf(_(" -k, --data-checksums use data page checksums\n")); + printf(_(" -K, --file-encryption-keylen\n" + " bit length of the file encryption key\n")); printf(_(" -L DIRECTORY where to find the input files\n")); printf(_(" -n, --no-clean do not clean up after errors\n")); printf(_(" -N, --no-sync do not wait for changes to be written safely to disk\n")); + printf(_(" -R, --authprompt prompt the user for a passphrase or PIN\n")); printf(_(" -s, --show show internal settings\n")); printf(_(" -S, --sync-only only sync data directory\n")); printf(_("\nOther options:\n")); *************** initialize_data_directory(void) *** 2860,2865 **** --- 2890,2912 ---- /* Top level PG_VERSION is checked by bootstrapper, so make it first */ write_version_file(NULL); + if (pass_terminal_fd) + { + #ifndef WIN32 + int terminal_fd = open("/dev/tty", O_RDWR, 0); + #else + int terminal_fd = open("CONOUT$", O_RDWR, 0); + #endif + + if (terminal_fd < 0) + { + pg_log_error(_("%s: could not open terminal: %s\n"), + progname, strerror(errno)); + exit(1); + } + term_fd_opt = psprintf("-R %d", terminal_fd); + } + /* Select suitable configuration settings */ set_null_conf(); test_config_settings(); *************** initialize_data_directory(void) *** 2883,2890 **** fflush(stdout); snprintf(cmd, sizeof(cmd), ! "\"%s\" %s template1 >%s", backend_exec, backend_options, DEVNULL); PG_CMD_OPEN; --- 2930,2938 ---- fflush(stdout); snprintf(cmd, sizeof(cmd), ! "\"%s\" %s %s template1 >%s", backend_exec, backend_options, + term_fd_opt ? term_fd_opt : "", DEVNULL); PG_CMD_OPEN; *************** main(int argc, char *argv[]) *** 2957,2963 **** --- 3005,3015 ---- {"waldir", required_argument, NULL, 'X'}, {"wal-segsize", required_argument, NULL, 12}, {"data-checksums", no_argument, NULL, 'k'}, + {"authprompt", no_argument, NULL, 'R'}, + {"file-encryption-keylen", no_argument, NULL, 'K'}, {"allow-group-access", no_argument, NULL, 'g'}, + {"cluster-passphrase-command", required_argument, NULL, 'c'}, + {"copy-encryption-keys", required_argument, NULL, 'u'}, {NULL, 0, NULL, 0} }; *************** main(int argc, char *argv[]) *** 2999,3005 **** /* process command-line options */ ! while ((c = getopt_long(argc, argv, "A:dD:E:gkL:nNsST:U:WX:", long_options, &option_index)) != -1) { switch (c) { --- 3051,3057 ---- /* process command-line options */ ! while ((c = getopt_long(argc, argv, "A:c:dD:E:gkK:L:nNRsST:u:U:WX:", long_options, &option_index)) != -1) { switch (c) { *************** main(int argc, char *argv[]) *** 3045,3050 **** --- 3097,3108 ---- case 'N': do_sync = false; break; + case 'R': + pass_terminal_fd = true; + break; + case 'K': + file_encryption_keylen = atoi(optarg); + break; case 'S': sync_only = true; break; *************** main(int argc, char *argv[]) *** 3081,3086 **** --- 3139,3150 ---- case 9: pwfilename = pg_strdup(optarg); break; + case 'c': + cluster_passphrase_cmd = pg_strdup(optarg); + break; + case 'u': + old_key_datadir = pg_strdup(optarg); + break; case 's': show_setting = true; break; *************** main(int argc, char *argv[]) *** 3151,3156 **** --- 3215,3251 ---- exit(1); } + #ifndef USE_OPENSSL + if (cluster_passphrase_cmd) + { + pg_log_error("cluster file encryption is not supported because OpenSSL is not supported by this build"); + exit(1); + } + #endif + + if (old_key_datadir != NULL && cluster_passphrase_cmd == NULL) + { + pg_log_error("copying encryption keys requires the cluster passphrase command to be specified"); + exit(1); + } + + if (file_encryption_keylen != 0 && cluster_passphrase_cmd == NULL) + { + pg_log_error("a non-zero file encryption key length requires the cluster passphrase command to be specified"); + exit(1); + } + + if (file_encryption_keylen != 0 && file_encryption_keylen != 128 && + file_encryption_keylen != 192 && file_encryption_keylen != 256) + { + pg_log_error("invalid file encrypt key length; supported values are 0 (disabled), 128, 192, and 256"); + exit(1); + } + + /* set the default */ + if (file_encryption_keylen == 0 && cluster_passphrase_cmd != NULL) + file_encryption_keylen = 128; + check_authmethod_unspecified(&authmethodlocal); check_authmethod_unspecified(&authmethodhost); *************** main(int argc, char *argv[]) *** 3218,3223 **** --- 3313,3323 ---- else printf(_("Data page checksums are disabled.\n")); + if (cluster_passphrase_cmd) + printf(_("Cluster file encryption is enabled.\n")); + else + printf(_("Cluster file encryption is disabled.\n")); + if (pwprompt || pwfilename) get_su_pwd(); diff --git a/src/bin/pg_altercpass/.gitignore b/src/bin/pg_altercpass/.gitignore new file mode 100644 index ...fe7160f *** a/src/bin/pg_altercpass/.gitignore --- b/src/bin/pg_altercpass/.gitignore *************** *** 0 **** --- 1,2 ---- + /pg_altercpass + diff --git a/src/bin/pg_altercpass/Makefile b/src/bin/pg_altercpass/Makefile new file mode 100644 index ...0a2b815 *** a/src/bin/pg_altercpass/Makefile --- b/src/bin/pg_altercpass/Makefile *************** *** 0 **** --- 1,44 ---- + #------------------------------------------------------------------------- + # + # Makefile for src/bin/pg_altercpass + # + # Copyright (c) 1998-2020, PostgreSQL Global Development Group + # + # src/bin/pg_altercpass/Makefile + # + #------------------------------------------------------------------------- + + PGFILEDESC = "pg_altercpass - alter the cluster passphrase" + PGAPPICON=win32 + + subdir = src/bin/pg_altercpass + top_builddir = ../../.. + include $(top_builddir)/src/Makefile.global + + OBJS = \ + $(WIN32RES) \ + pg_altercpass.o + + all: pg_altercpass + + pg_altercpass: $(OBJS) | submake-libpgport + $(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X) + + install: all installdirs + $(INSTALL_PROGRAM) pg_altercpass$(X) '$(DESTDIR)$(bindir)/pg_altercpass$(X)' + + installdirs: + $(MKDIR_P) '$(DESTDIR)$(bindir)' + + uninstall: + rm -f '$(DESTDIR)$(bindir)/pg_altercpass$(X)' + + clean distclean maintainer-clean: + rm -f pg_altercpass$(X) $(OBJS) + rm -rf tmp_check + + check: + $(prove_check) + + installcheck: + $(prove_installcheck) diff --git a/src/bin/pg_altercpass/pg_altercpass.c b/src/bin/pg_altercpass/pg_altercpass.c new file mode 100644 index ...8032fa0 *** a/src/bin/pg_altercpass/pg_altercpass.c --- b/src/bin/pg_altercpass/pg_altercpass.c *************** *** 0 **** --- 1,669 ---- + /*------------------------------------------------------------------------- + * + * pg_altercpass.c + * A utility to change the cluster passphrase (key encryption key, KEK) + * used for cluster file encryption. + * + * The theory of operation is fairly simple: + * 1. Create lock file + * 2. Retrieve current and new cluster passphrase using the supplied + * commands. + * 3. Revert any failed alter operation. + * 4. Create a temporary directory in PGDATA + * 5. For each data encryption key in the pg_cryptokeys directory, + * descrypt it with the old cluster passphrase and reencrypt it + * with the new cluster passphrase. + * 6. Make the temporary directory the new pg_cryptokeys directory. + * 7. Remove lock file + * + * + * Portions Copyright (c) 1996-2020, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/bin/pg_altercpass/pg_altercpass.c + * + *------------------------------------------------------------------------- + */ + + + #define FRONTEND 1 + + #include "postgres_fe.h" + + #include + #include + #include + + #include "common/file_perm.h" + #include "common/file_utils.h" + #include "common/restricted_token.h" + #include "crypto/kmgr.h" + #include "common/logging.h" + #include "getopt_long.h" + #include "pg_getopt.h" + + + static int lock_fd = -1; + static bool pass_terminal_fd = false; + int terminal_fd = -1; + static bool repair_mode = false; + static char *old_cluster_passphrase_cmd = NULL, + *new_cluster_passphrase_cmd = NULL; + static char old_passphrase[KMGR_MAX_PASSPHRASE_LEN], + new_passphrase[KMGR_MAX_PASSPHRASE_LEN]; + static int old_passlen, new_passlen; + static CryptoKey in_key, data_key, out_key; + static char top_path[MAXPGPATH], pid_path[MAXPGPATH], live_path[MAXPGPATH], + new_path[MAXPGPATH], old_path[MAXPGPATH]; + + static char *DataDir = NULL; + static const char *progname; + + static void create_lockfile(void); + static void recover_failure(void); + static void retrieve_passphrases(void); + static void bzero_keys_and_exit(int exit_code); + static void reencrypt_data_keys(void); + static void install_new_keys(void); + + static void + usage(const char *progname) + { + printf(_("%s changes the cluster passphrase of a PostgreSQL database cluster.\n\n"), progname); + printf(_("Usage:\n")); + printf(_(" %s [OPTION] old_cluster_passphrase_command new_cluster_passphrase_command [DATADIR]\n"), progname); + printf(_(" %s [repair_option] [DATADIR]\n"), progname); + printf(_("\nOptions:\n")); + printf(_(" -R, --authprompt prompt for a passphrases or PIN\n")); + printf(_(" [-D, --pgdata=]DATADIR data directory\n")); + printf(_(" -V, --version output version information, then exit\n")); + printf(_(" -?, --help show this help, then exit\n")); + printf(_("\nRepair options:\n")); + printf(_(" -r, --repair repair previous failure\n")); + printf(_("\nIf no data directory (DATADIR) is specified, " + "the environment variable PGDATA\nis used.\n\n")); + printf(_("Report bugs to <%s>.\n"), PACKAGE_BUGREPORT); + printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL); + } + + + int + main(int argc, char *argv[]) + { + static struct option long_options1[] = { + {"authprompt", required_argument, NULL, 'R'}, + {"repair", required_argument, NULL, 'r'}, + {NULL, 0, NULL, 0} + }; + + static struct option long_options2[] = { + {"pgdata", required_argument, NULL, 'D'}, + {NULL, 0, NULL, 0} + }; + + int c; + + pg_logging_init(argv[0]); + set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pg_altercpass")); + progname = get_progname(argv[0]); + + if (argc > 1) + { + if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0) + { + usage(progname); + exit(0); + } + if (strcmp(argv[1], "--version") == 0 || strcmp(argv[1], "-V") == 0) + { + puts("pg_altercpass (PostgreSQL) " PG_VERSION); + exit(0); + } + } + + /* check for -r/-R */ + while ((c = getopt_long(argc, argv, "rR", long_options1, NULL)) != -1) + { + switch (c) + { + case 'r': + repair_mode = true; + break; + + case 'R': + pass_terminal_fd = true; + break; + + default: + fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname); + exit(1); + } + } + + if (!repair_mode) + { + /* get cluser passphrase commands */ + if (optind < argc) + old_cluster_passphrase_cmd = argv[optind++]; + else + { + pg_log_error("missing old_cluster_passphrase_command"); + fprintf(stderr, _("Try \"%s --help\" for more information.\n"), + progname); + exit(1); + } + + if (optind < argc) + new_cluster_passphrase_cmd = argv[optind++]; + else + { + pg_log_error("missing new_cluster_passphrase_command"); + fprintf(stderr, _("Try \"%s --help\" for more information.\n"), + progname); + exit(1); + } + } + + /* check for datadir */ + argc -= optind; + argv += optind; + + while ((c = getopt_long(argc, argv, "D:", long_options2, NULL)) != -1) + { + switch (c) + { + case 'D': + DataDir = optarg; + break; + + default: + fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname); + exit(1); + } + } + + if (DataDir == NULL) + { + if (optind < argc) + DataDir = argv[optind++]; + else + DataDir = getenv("PGDATA"); + } + + /* + * Disallow running as root because we create directories in PGDATA + */ + #ifndef WIN32 + if (geteuid() == 0) + { + pg_log_error("%s: cannot be run as root\n" + "Please log in (using, e.g., \"su\") as the " + "(unprivileged) user that will\n" + "own the server process.\n", + progname); + exit(1); + } + #endif + + get_restricted_token(); + + /* Set mask based on PGDATA permissions */ + if (!GetDataDirectoryCreatePerm(DataDir)) + { + pg_log_error("could not read permissions of directory \"%s\": %m", + DataDir); + exit(1); + } + + umask(pg_mode_mask); + + snprintf(top_path, sizeof(top_path), "%s/%s", DataDir, KMGR_DIR); + snprintf(pid_path, sizeof(pid_path), "%s/%s", DataDir, KMGR_DIR_PID); + snprintf(live_path, sizeof(live_path), "%s/%s", DataDir, LIVE_KMGR_DIR); + snprintf(new_path, sizeof(new_path), "%s/%s", DataDir, NEW_KMGR_DIR); + snprintf(old_path, sizeof(old_path), "%s/%s", DataDir, OLD_KMGR_DIR); + + /* Complain if any arguments remain */ + if (optind < argc) + { + pg_log_error("too many command-line arguments (first is \"%s\")", + argv[optind]); + fprintf(stderr, _("Try \"%s --help\" for more information.\n"), + progname); + exit(1); + } + + if (DataDir == NULL) + { + pg_log_error("no data directory specified"); + fprintf(stderr, _("Try \"%s --help\" for more information.\n"), progname); + exit(1); + } + + create_lockfile(); + + recover_failure(); + + if (!repair_mode) + { + retrieve_passphrases(); + reencrypt_data_keys(); + install_new_keys(); + } + + #ifndef WIN32 + /* remove file system reference to file */ + if (unlink(pid_path) < 0) + { + pg_log_error("could not delete lock file \"%s\": %m", KMGR_DIR_PID); + exit(1); + } + #endif + + close (lock_fd); + + bzero_keys_and_exit(0); + } + + /* This prevents almost all cases of concurrent access */ + void + create_lockfile(void) + { + struct stat buffer; + char lock_pid_str[20]; + + if (stat(top_path, &buffer) != 0 || !S_ISDIR(buffer.st_mode)) + { + pg_log_error("cluster file encryption directory \"%s\" is missing; is it enabled?", KMGR_DIR_PID); + fprintf(stderr, _("Exiting with no changes made.\n")); + exit(1); + } + + /* Does a lockfile exist? */ + if ((lock_fd = open(pid_path, O_RDONLY, 0)) != -1) + { + int lock_pid; + int len; + + /* read the PID */ + if ((len = read(lock_fd, lock_pid_str, sizeof(lock_pid_str) - 1)) == 0) + { + pg_log_error("cannot read pid from lock file \"%s\": %m", KMGR_DIR_PID); + fprintf(stderr, _("Exiting with no changes made.\n")); + exit(1); + } + lock_pid_str[len] = '\0'; + + if ((lock_pid = atoi(lock_pid_str)) == 0) + { + pg_log_error("invalid pid in lock file \"%s\": %m", KMGR_DIR_PID); + fprintf(stderr, _("Exiting with no changes made.\n")); + exit(1); + } + + /* Is the PID running? */ + if (kill(lock_pid, 0) == 0) + { + pg_log_error("active process %d currently holds a lock on this operation, recorded in \"%s\"", + lock_pid, KMGR_DIR_PID); + fprintf(stderr, _("Exiting with no changes made.\n")); + exit(1); + } + + close(lock_fd); + + if (repair_mode) + printf("old lock file removed\n"); + + /* + * pid is no longer running, so remove the lock file. + * This is not 100% safe from concurrent access, e.g.: + * + * process 1 exits and leaves stale lock file + * process 2 checks stale lock file of process 1 + * process 3 checks stale lock file of process 1 + * process 2 remove the lock file of process 1 + * process 4 creates a lock file + * process 3 remove the lock file of process 4 + * process 5 creates a lock file + * + * The sleep(2) helps with this since it reduces the likelyhood + * a process that did an unlock will interfere with another unlock + * process. We could ask users to remove the lock, but that seems + * even more error-prone, especially since this might happen + * on server start. Many PG tools seem to have problems with + * concurrent access. + */ + unlink(pid_path); + + /* Sleep to reduce the likelihood of concurrent unlink */ + sleep(2); + } + + /* Create our own lockfile? */ + lock_fd = open(pid_path, O_RDWR | O_CREAT | O_EXCL + #ifdef WIN32 + /* delete on close */ + | O_TEMPORARY + #endif + , pg_file_create_mode); + + if (lock_fd == -1) + { + if (errno == EEXIST) + pg_log_error("an active process currently holds a lock on this operation, recorded in \"%s\"", + KMGR_DIR_PID); + else + pg_log_error("unable to create lock file \"%s\": %m", KMGR_DIR_PID); + fprintf(stderr, _("Exiting with no changes made.\n")); + exit(1); + } + + snprintf(lock_pid_str, sizeof(lock_pid_str), "%d\n", getpid()); + if (write(lock_fd, lock_pid_str, strlen(lock_pid_str)) != strlen(lock_pid_str)) + { + pg_log_error("could not write pid to lock file \"%s\": %m", KMGR_DIR_PID); + fprintf(stderr, _("Exiting with no changes made.\n")); + exit(1); + } + } + + /* + * recover_failure + * + * A previous pg_altercpass might have failed, so it might need recovery. + * The normal operation is: + * 1. reencrypt LIVE_KMGR_DIR -> NEW_KMGR_DIR + * 2. rename KMGR_DIR -> OLD_KMGR_DIR + * 3. rename NEW_KMGR_DIR -> LIVE_KMGR_DIR + * remove OLD_KMGR_DIR + * + * There are eight possible directory configurations: + * + * LIVE_KMGR_DIR NEW_KMGR_DIR OLD_KMGR_DIR + * + * Normal: + * 0. normal X + * 1. remove new X X + * 2. install new X X + * 3. remove old X X + * + * Abnormal: + * fatal + * restore old X + * install new X + * remove old and new X X X + * + * We don't handle the abnormal cases, just report an error. + */ + static void + recover_failure(void) + { + struct stat buffer; + bool is_live, is_new, is_old; + + is_live = !stat(live_path, &buffer); + is_new = !stat(new_path, &buffer); + is_old = !stat(old_path, &buffer); + + /* normal #0 */ + if (is_live && !is_new && !is_old) + { + if (repair_mode) + printf("repair unnecessary\n"); + return; + } + /* remove new #1 */ + else if (is_live && is_new && !is_old) + { + if (!rmtree(new_path, true)) + { + pg_log_error("unable to remove new directory \"%s\": %m", NEW_KMGR_DIR); + fprintf(stderr, _("Exiting with no changes made.\n")); + exit(1); + } + printf(_("removed files created during previously aborted alter operation\n")); + return; + } + /* install new #2 */ + else if (!is_live && is_new && is_old) + { + if (rename(new_path, live_path) != 0) + { + pg_log_error("unable to rename directory \"%s\" to \"%s\": %m", + NEW_KMGR_DIR, LIVE_KMGR_DIR); + fprintf(stderr, _("Exiting with no changes made.\n")); + exit(1); + } + printf(_("Installed new cluster password supplied in previous alter operation\n")); + return; + } + /* remove old #3 */ + else if (is_live && !is_new && is_old) + { + if (!rmtree(old_path, true)) + { + pg_log_error("unable to remove old directory \"%s\": %m", OLD_KMGR_DIR); + fprintf(stderr, _("Exiting with no changes made.\n")); + exit(1); + } + printf(_("Removed old files invalidated during previous alter operation\n")); + return; + } + else + { + pg_log_error("cluster file encryption directory \"%s\" is in an abnormal state and cannot be processed", + KMGR_DIR); + fprintf(stderr, _("Exiting with no changes made.\n")); + exit(1); + } + } + + /* Retrieve old and new passphrases */ + void + retrieve_passphrases() + { + /* + * If we have been asked to pass an open file descriptor to the user + * terminal to the commands, set one up. + */ + if (pass_terminal_fd) + { + #ifndef WIN32 + terminal_fd = open("/dev/tty", O_RDWR, 0); + #else + terminal_fd = open("CONOUT$", O_RDWR, 0); + #endif + + if (terminal_fd < 0) + { + pg_log_error(_("%s: could not open terminal: %s\n"), + progname, strerror(errno)); + exit(1); + } + } + + /* Get old key encryption key from the passphrase command */ + old_passlen = kmgr_run_cluster_passphrase_command(old_cluster_passphrase_cmd, + old_passphrase, KMGR_MAX_PASSPHRASE_LEN, + live_path); + if (old_passlen < KMGR_MIN_PASSPHRASE_LEN) + { + pg_log_error("passphrase must be at least %d bytes", KMGR_MIN_PASSPHRASE_LEN); + bzero_keys_and_exit(1); + } + + /* + * Create new key directory here in case the passphrase command needs it + * to exist. + */ + if (mkdir(new_path, pg_dir_create_mode) != 0) + { + pg_log_error("unable to create new cluster passphrase directory \"%s\": %m", NEW_KMGR_DIR); + bzero_keys_and_exit(1); + } + + /* Get new key */ + new_passlen = kmgr_run_cluster_passphrase_command(new_cluster_passphrase_cmd, + new_passphrase, KMGR_MAX_PASSPHRASE_LEN, + new_path); + if (new_passlen < KMGR_MIN_PASSPHRASE_LEN) + { + pg_log_error("passphrase must be at least %d bytes", KMGR_MIN_PASSPHRASE_LEN); + bzero_keys_and_exit(1); + } + + if (pass_terminal_fd) + close(terminal_fd); + + /* output newline */ + puts(""); + + if (strcmp(old_passphrase, new_passphrase) == 0) + { + pg_log_error("passphrases are identical, exiting\n"); + bzero_keys_and_exit(1); + } + + } + + /* Decrypt old keys encrypted with old pass phrase and reencrypt with new one */ + void + reencrypt_data_keys(void) + { + DIR *dir; + struct dirent *de; + PgKeyWrapCtx *old_ctx, *new_ctx; + uint8 old_enckey[KMGR_ENC_KEY_LEN], new_enckey[KMGR_ENC_KEY_LEN]; + uint8 old_hmackey[KMGR_MAC_KEY_LEN], new_hmackey[KMGR_MAC_KEY_LEN]; + + if ((dir = opendir(live_path)) == NULL) + { + pg_log_error("unable to open live cluster passphrase directory \"%s\": %m", LIVE_KMGR_DIR); + bzero_keys_and_exit(1); + } + + kmgr_derive_keys(old_passphrase, old_passlen, old_enckey, old_hmackey); + old_ctx = pg_create_keywrap_ctx(old_enckey, old_hmackey); + + kmgr_derive_keys(new_passphrase, new_passlen, new_enckey, new_hmackey); + new_ctx = pg_create_keywrap_ctx(new_enckey, new_hmackey); + + while ((de = readdir(dir)) != NULL) + { + /* + * We copy only the numeric files/keys, since there might be encrypted passphrase + * files in the old directory that only match the old key. + */ + if (strspn(de->d_name, "0123456789") == strlen(de->d_name)) + { + char src_path[MAXPGPATH], dst_path[MAXPGPATH]; + int src_fd, dst_fd; + int len; + uint32 id = strtoul(de->d_name, NULL, 10); + + CryptoKeyFilePath(src_path, live_path, id); + CryptoKeyFilePath(dst_path, new_path, id); + + if ((src_fd = open(src_path, O_RDONLY | PG_BINARY, 0)) < 0) + { + pg_log_error("could not open file \"%s\": %m", src_path); + bzero_keys_and_exit(1); + } + + if ((dst_fd = open(dst_path, O_RDWR | O_CREAT | O_TRUNC | PG_BINARY, + pg_file_create_mode)) < 0) + { + pg_log_error("could not open file \"%s\": %m", dst_path); + bzero_keys_and_exit(1); + } + + /* Read the source key */ + len = read(src_fd, &in_key, sizeof(CryptoKey)); + if (len != sizeof(CryptoKey)) + { + if (len < 0) + pg_log_error("could read file \"%s\": %m", src_path); + else + pg_log_error("could read file \"%s\": read %d of %zu", + src_path, len, sizeof(CryptoKey)); + bzero_keys_and_exit(1); + } + + /* decrypt with old key */ + if (!kmgr_unwrap_key(old_ctx, &in_key, &data_key)) + { + pg_log_error("incorrect old key specified"); + bzero_keys_and_exit(1); + } + + /* encrypt with new key */ + if (!kmgr_wrap_key(new_ctx, &data_key, &out_key)) + { + pg_log_error("could not encrypt new key"); + bzero_keys_and_exit(1); + } + + /* Write to the dest key */ + len = write(dst_fd, &out_key, sizeof(CryptoKey)); + if (len != sizeof(CryptoKey)) + { + pg_log_error("could not write fie \"%s\"", dst_path); + bzero_keys_and_exit(1); + } + + close(src_fd); + close(dst_fd); + } + } + + /* The passphrase is correct, free the cipher context */ + pg_free_keywrap_ctx(old_ctx); + pg_free_keywrap_ctx(new_ctx); + + closedir(dir); + } + + void + install_new_keys(void) + { + /* add fsyncs? XXX */ + if (rename(live_path, old_path) != 0) + { + pg_log_error("unable to rename directory \"%s\" to \"%s\": %m", + LIVE_KMGR_DIR, OLD_KMGR_DIR); + bzero_keys_and_exit(1); + } + + if (rename(new_path, live_path) != 0) + { + pg_log_error("unable to rename directory \"%s\" to \"%s\": %m", + NEW_KMGR_DIR, LIVE_KMGR_DIR); + bzero_keys_and_exit(1); + } + + if (!rmtree(old_path, true)) + { + pg_log_error("unable to remove old directory \"%s\": %m", OLD_KMGR_DIR); + bzero_keys_and_exit(1); + } + } + + void + bzero_keys_and_exit(int exit_code) + { + explicit_bzero(old_passphrase, sizeof(old_passphrase)); + explicit_bzero(new_passphrase, sizeof(new_passphrase)); + + explicit_bzero(&in_key, sizeof(in_key)); + explicit_bzero(&data_key, sizeof(data_key)); + explicit_bzero(&out_key, sizeof(out_key)); + + if (exit_code != 0) + { + unlink(pid_path); + printf("Re-running pg_altercpass to repair might be needed before the next server start\n"); + } + + exit(exit_code); + } diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c new file mode 100644 index 3e00ac0..c3b38b7 *** a/src/bin/pg_controldata/pg_controldata.c --- b/src/bin/pg_controldata/pg_controldata.c *************** *** 25,30 **** --- 25,31 ---- #include "access/xlog_internal.h" #include "catalog/pg_control.h" #include "common/controldata_utils.h" + #include "common/kmgr_utils.h" #include "common/logging.h" #include "getopt_long.h" #include "pg_getopt.h" *************** main(int argc, char *argv[]) *** 334,338 **** --- 335,341 ---- ControlFile->data_checksum_version); printf(_("Mock authentication nonce: %s\n"), mock_auth_nonce_str); + printf(_("File encryption key length: %d\n"), + ControlFile->file_encryption_keylen); return 0; } diff --git a/src/bin/pg_ctl/pg_ctl.c b/src/bin/pg_ctl/pg_ctl.c new file mode 100644 index fc07f1a..5fa1f72 *** a/src/bin/pg_ctl/pg_ctl.c --- b/src/bin/pg_ctl/pg_ctl.c *************** typedef enum *** 79,84 **** --- 79,85 ---- static bool do_wait = true; static int wait_seconds = DEFAULT_WAIT; static bool wait_seconds_arg = false; + static bool pass_terminal_fd = false; static bool silent_mode = false; static ShutdownMode shutdown_mode = FAST_MODE; static int sig = SIGINT; /* default */ *************** free_readfile(char **optlines) *** 442,448 **** static pgpid_t start_postmaster(void) { ! char cmd[MAXPGPATH]; #ifndef WIN32 pgpid_t pm_pid; --- 443,449 ---- static pgpid_t start_postmaster(void) { ! char cmd[MAXPGPATH], *term_fd_opt = NULL; #ifndef WIN32 pgpid_t pm_pid; *************** start_postmaster(void) *** 467,472 **** --- 468,486 ---- /* fork succeeded, in child */ + if (pass_terminal_fd) + { + int terminal_fd = open("/dev/tty", O_RDWR, 0); + + if (terminal_fd < 0) + { + write_stderr(_("%s: could not open terminal: %s\n"), + progname, strerror(errno)); + exit(1); + } + term_fd_opt = psprintf(" -R %d", terminal_fd); + } + /* * If possible, detach the postmaster process from the launching process * group and make it a group leader, so that it doesn't get signaled along *************** start_postmaster(void) *** 487,498 **** * has the same PID as the current child process. */ if (log_file != NULL) ! snprintf(cmd, MAXPGPATH, "exec \"%s\" %s%s < \"%s\" >> \"%s\" 2>&1", exec_path, pgdata_opt, post_opts, DEVNULL, log_file); else ! snprintf(cmd, MAXPGPATH, "exec \"%s\" %s%s < \"%s\" 2>&1", ! exec_path, pgdata_opt, post_opts, DEVNULL); (void) execl("/bin/sh", "/bin/sh", "-c", cmd, (char *) NULL); --- 501,514 ---- * has the same PID as the current child process. */ if (log_file != NULL) ! snprintf(cmd, MAXPGPATH, "exec \"%s\" %s%s%s < \"%s\" >> \"%s\" 2>&1", exec_path, pgdata_opt, post_opts, + term_fd_opt ? term_fd_opt : "", DEVNULL, log_file); else ! snprintf(cmd, MAXPGPATH, "exec \"%s\" %s%s%s < \"%s\" 2>&1", ! exec_path, pgdata_opt, post_opts, ! term_fd_opt ? term_fd_opt : "", DEVNULL); (void) execl("/bin/sh", "/bin/sh", "-c", cmd, (char *) NULL); *************** start_postmaster(void) *** 513,518 **** --- 529,549 ---- PROCESS_INFORMATION pi; const char *comspec; + if (pass_terminal_fd) + { + /* Hopefully we can read and write CONOUT, see simple_prompt() XXX */ + /* Do CreateRestrictedProcess() children even inherit open file descriptors? XXX */ + int terminal_fd = open("CONOUT$", O_RDWR, 0); + + if (terminal_fd < 0) + { + write_stderr(_("%s: could not open terminal: %s\n"), + progname, strerror(errno)); + exit(1); + } + term_fd_opt = psprintf(" -R %d", terminal_fd); + } + /* Find CMD.EXE location using COMSPEC, if it's set */ comspec = getenv("COMSPEC"); if (comspec == NULL) *************** start_postmaster(void) *** 553,564 **** else close(fd); ! snprintf(cmd, MAXPGPATH, "\"%s\" /C \"\"%s\" %s%s < \"%s\" >> \"%s\" 2>&1\"", ! comspec, exec_path, pgdata_opt, post_opts, DEVNULL, log_file); } else ! snprintf(cmd, MAXPGPATH, "\"%s\" /C \"\"%s\" %s%s < \"%s\" 2>&1\"", ! comspec, exec_path, pgdata_opt, post_opts, DEVNULL); if (!CreateRestrictedProcess(cmd, &pi, false)) { --- 584,597 ---- else close(fd); ! snprintf(cmd, MAXPGPATH, "\"%s\" /C \"\"%s\" %s%s%s < \"%s\" >> \"%s\" 2>&1\"", ! comspec, exec_path, pgdata_opt, post_opts, ! term_fd_opt ? term_fd_opt : "", DEVNULL, log_file); } else ! snprintf(cmd, MAXPGPATH, "\"%s\" /C \"\"%s\" %s%s%s < \"%s\" 2>&1\"", ! comspec, exec_path, pgdata_opt, post_opts, ! term_fd_opt ? term_fd_opt : "", DEVNULL); if (!CreateRestrictedProcess(cmd, &pi, false)) { *************** wait_for_postmaster(pgpid_t pm_pid, bool *** 689,695 **** } else #endif ! print_msg("."); } pg_usleep(USEC_PER_SEC / WAITS_PER_SEC); --- 722,729 ---- } else #endif ! if (!pass_terminal_fd) ! print_msg("."); } pg_usleep(USEC_PER_SEC / WAITS_PER_SEC); *************** do_help(void) *** 2066,2071 **** --- 2100,2106 ---- printf(_(" -o, --options=OPTIONS command line options to pass to postgres\n" " (PostgreSQL server executable) or initdb\n")); printf(_(" -p PATH-TO-POSTGRES normally not necessary\n")); + printf(_(" -R, --authprompt prompt for a paasphrase or PIN\n")); printf(_("\nOptions for stop or restart:\n")); printf(_(" -m, --mode=MODE MODE can be \"smart\", \"fast\", or \"immediate\"\n")); *************** main(int argc, char **argv) *** 2260,2265 **** --- 2295,2301 ---- {"mode", required_argument, NULL, 'm'}, {"pgdata", required_argument, NULL, 'D'}, {"options", required_argument, NULL, 'o'}, + {"authprompt", no_argument, NULL, 'R'}, {"silent", no_argument, NULL, 's'}, {"timeout", required_argument, NULL, 't'}, {"core-files", no_argument, NULL, 'c'}, *************** main(int argc, char **argv) *** 2332,2338 **** /* process command-line options */ while (optind < argc) { ! while ((c = getopt_long(argc, argv, "cD:e:l:m:N:o:p:P:sS:t:U:wW", long_options, &option_index)) != -1) { switch (c) --- 2368,2374 ---- /* process command-line options */ while (optind < argc) { ! while ((c = getopt_long(argc, argv, "cD:e:l:m:N:o:p:P:RsS:t:U:wW", long_options, &option_index)) != -1) { switch (c) *************** main(int argc, char **argv) *** 2385,2390 **** --- 2421,2429 ---- case 'P': register_password = pg_strdup(optarg); break; + case 'R': + pass_terminal_fd = true; + break; case 's': silent_mode = true; break; diff --git a/src/bin/pg_resetwal/pg_resetwal.c b/src/bin/pg_resetwal/pg_resetwal.c new file mode 100644 index cb6ef19..8f928b3 *** a/src/bin/pg_resetwal/pg_resetwal.c --- b/src/bin/pg_resetwal/pg_resetwal.c *************** PrintControlValues(bool guessed) *** 804,809 **** --- 804,811 ---- (ControlFile.float8ByVal ? _("by value") : _("by reference"))); printf(_("Data page checksum version: %u\n"), ControlFile.data_checksum_version); + printf(_("File encryption key length: %d\n"), + ControlFile.file_encryption_keylen); } diff --git a/src/bin/pg_rewind/filemap.c b/src/bin/pg_rewind/filemap.c new file mode 100644 index ba34dba..87b09db *** a/src/bin/pg_rewind/filemap.c --- b/src/bin/pg_rewind/filemap.c *************** *** 28,33 **** --- 28,34 ---- #include "catalog/pg_tablespace_d.h" #include "common/hashfn.h" + #include "common/kmgr_utils.h" #include "common/string.h" #include "datapagemap.h" #include "filemap.h" *************** static const char *excludeDirContents[] *** 108,113 **** --- 109,121 ---- "pg_notify", /* + * Skip cryptographic keys. It's generally not a good idea to copy the + * cryptographic keys from source database because these might use + * different cluster passphrase. + */ + KMGR_DIR, + + /* * Old contents are loaded for possible debugging but are not required for * normal operation, see SerialInit(). */ diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c new file mode 100644 index 6685d51..5951f1e *** a/src/bin/pg_upgrade/check.c --- b/src/bin/pg_upgrade/check.c *************** *** 10,15 **** --- 10,16 ---- #include "postgres_fe.h" #include "catalog/pg_authid_d.h" + #include "common/kmgr_utils.h" #include "fe_utils/string_utils.h" #include "mb/pg_wchar.h" #include "pg_upgrade.h" *************** static void check_for_tables_with_oids(C *** 27,32 **** --- 28,34 ---- static void check_for_reg_data_type_usage(ClusterInfo *cluster); static void check_for_jsonb_9_4_usage(ClusterInfo *cluster); static void check_for_pg_role_prefix(ClusterInfo *cluster); + static void check_for_cluster_passphrase_failure(ClusterInfo *cluster); static void check_for_new_tablespace_dir(ClusterInfo *new_cluster); static char *get_canonical_locale_name(int category, const char *locale); *************** check_and_dump_old_cluster(bool live_che *** 139,144 **** --- 141,149 ---- if (GET_MAJOR_VERSION(old_cluster.major_version) <= 905) check_for_pg_role_prefix(&old_cluster); + if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1400) + check_for_cluster_passphrase_failure(&old_cluster); + if (GET_MAJOR_VERSION(old_cluster.major_version) == 904 && old_cluster.controldata.cat_ver < JSONB_FORMAT_CHANGE_CAT_VER) check_for_jsonb_9_4_usage(&old_cluster); *************** check_new_cluster(void) *** 173,178 **** --- 178,186 ---- check_loadable_libraries(); + if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1400) + check_for_cluster_passphrase_failure(&new_cluster); + switch (user_opts.transfer_mode) { case TRANSFER_MODE_CLONE: *************** check_for_pg_role_prefix(ClusterInfo *cl *** 1267,1272 **** --- 1275,1306 ---- check_ok(); } + + + /* + * check_for_cluster_passphrase_failure() + * + * Make sure there was no unrepaired pg_altercpass failure + */ + static void + check_for_cluster_passphrase_failure(ClusterInfo *cluster) + { + struct stat buffer; + + if (stat (KMGR_DIR_PID, &buffer) == 0) + { + if (cluster == &old_cluster) + pg_fatal("The source cluster had a pg_altercpass failure that needs repair or\n" + "pg_altercpass is running. Run pg_altercpass --repair or wait for it\n" + "to complete.\n"); + else + pg_fatal("The target cluster had a pg_altercpass failure that needs repair or\n" + "pg_altercpass is running. Run pg_altercpass --repair or wait for it\n" + "to complete.\n"); + } + + check_ok(); + } /* diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c new file mode 100644 index 39bcaa8..a0aa995 *** a/src/bin/pg_upgrade/controldata.c --- b/src/bin/pg_upgrade/controldata.c *************** *** 9,18 **** --- 9,24 ---- #include "postgres_fe.h" + #include #include #include "pg_upgrade.h" + #include "access/xlog_internal.h" + #include "common/controldata_utils.h" + #include "common/file_utils.h" + #include "common/kmgr_utils.h" + /* * get_control_data() * *************** get_control_data(ClusterInfo *cluster, b *** 59,64 **** --- 65,71 ---- bool got_date_is_int = false; bool got_data_checksum_version = false; bool got_cluster_state = false; + int got_file_encryption_keylen = 0; char *lc_collate = NULL; char *lc_ctype = NULL; char *lc_monetary = NULL; *************** get_control_data(ClusterInfo *cluster, b *** 202,207 **** --- 209,221 ---- got_data_checksum_version = true; } + /* Only in <= 14 */ + if (GET_MAJOR_VERSION(cluster->major_version) <= 1400) + { + cluster->controldata.file_encryption_keylen = 0; + got_file_encryption_keylen = true; + } + /* we have the result of cmd in "output". so parse it line by line now */ while (fgets(bufin, sizeof(bufin), output)) { *************** get_control_data(ClusterInfo *cluster, b *** 485,490 **** --- 499,516 ---- cluster->controldata.data_checksum_version = str2uint(p); got_data_checksum_version = true; } + else if ((p = strstr(bufin, "File encryption key length:")) != NULL) + { + p = strchr(p, ':'); + + if (p == NULL || strlen(p) <= 1) + pg_fatal("%d: controldata retrieval problem\n", __LINE__); + + p++; /* remove ':' char */ + /* used later for contrib check */ + cluster->controldata.file_encryption_keylen = atoi(p); + got_file_encryption_keylen = true; + } } pclose(output); *************** get_control_data(ClusterInfo *cluster, b *** 539,545 **** !got_index || !got_toast || (!got_large_object && cluster->controldata.ctrl_ver >= LARGE_OBJECT_SIZE_PG_CONTROL_VER) || ! !got_date_is_int || !got_data_checksum_version) { if (cluster == &old_cluster) pg_log(PG_REPORT, --- 565,572 ---- !got_index || !got_toast || (!got_large_object && cluster->controldata.ctrl_ver >= LARGE_OBJECT_SIZE_PG_CONTROL_VER) || ! !got_date_is_int || !got_data_checksum_version || ! !got_file_encryption_keylen) { if (cluster == &old_cluster) pg_log(PG_REPORT, *************** get_control_data(ClusterInfo *cluster, b *** 605,610 **** --- 632,641 ---- if (!got_data_checksum_version) pg_log(PG_REPORT, " data checksum version\n"); + /* value added in Postgres 14 */ + if (!got_file_encryption_keylen) + pg_log(PG_REPORT, " file encryption key length\n"); + pg_fatal("Cannot continue without required control information, terminating\n"); } } *************** check_control_data(ControlData *oldctrl, *** 669,674 **** --- 700,714 ---- pg_fatal("old cluster uses data checksums but the new one does not\n"); else if (oldctrl->data_checksum_version != newctrl->data_checksum_version) pg_fatal("old and new cluster pg_controldata checksum versions do not match\n"); + + /* + * We cannot upgrade if the old cluster file encryption key length + * doesn't match the new one. + + */ + if (oldctrl->file_encryption_keylen != newctrl->file_encryption_keylen) + pg_fatal("old and new clusters use different file encryption key lengths or\n" + "one cluster uses encryption and the other does not"); } diff --git a/src/bin/pg_upgrade/file.c b/src/bin/pg_upgrade/file.c new file mode 100644 index cc8a675..c985119 *** a/src/bin/pg_upgrade/file.c --- b/src/bin/pg_upgrade/file.c *************** *** 11,16 **** --- 11,17 ---- #include #include + #include #ifdef HAVE_COPYFILE_H #include #endif *************** *** 21,26 **** --- 22,28 ---- #include "access/visibilitymap.h" #include "common/file_perm.h" + #include "common/file_utils.h" #include "pg_upgrade.h" #include "storage/bufpage.h" #include "storage/checksum.h" diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c new file mode 100644 index 548d648..4702998 *** a/src/bin/pg_upgrade/option.c --- b/src/bin/pg_upgrade/option.c *************** parseCommandLine(int argc, char *argv[]) *** 52,57 **** --- 52,58 ---- {"check", no_argument, NULL, 'c'}, {"link", no_argument, NULL, 'k'}, {"retain", no_argument, NULL, 'r'}, + {"authprompt", no_argument, NULL, 'R'}, {"jobs", required_argument, NULL, 'j'}, {"socketdir", required_argument, NULL, 's'}, {"verbose", no_argument, NULL, 'v'}, *************** parseCommandLine(int argc, char *argv[]) *** 102,108 **** if (os_user_effective_id == 0) pg_fatal("%s: cannot be run as root\n", os_info.progname); ! while ((option = getopt_long(argc, argv, "d:D:b:B:cj:ko:O:p:P:rs:U:v", long_options, &optindex)) != -1) { switch (option) --- 103,109 ---- if (os_user_effective_id == 0) pg_fatal("%s: cannot be run as root\n", os_info.progname); ! while ((option = getopt_long(argc, argv, "d:D:b:B:cj:ko:O:p:P:rRs:U:v", long_options, &optindex)) != -1) { switch (option) *************** parseCommandLine(int argc, char *argv[]) *** 180,185 **** --- 181,190 ---- log_opts.retain = true; break; + case 'R': + user_opts.pass_terminal_fd = true; + break; + case 's': user_opts.socketdir = pg_strdup(optarg); break; diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h new file mode 100644 index ee70243..53ce195 *** a/src/bin/pg_upgrade/pg_upgrade.h --- b/src/bin/pg_upgrade/pg_upgrade.h *************** *** 11,16 **** --- 11,17 ---- #include #include "libpq-fe.h" + #include "common/kmgr_utils.h" /* Use port in the private/dynamic port number range */ #define DEF_PGUPORT 50432 *************** typedef struct *** 219,224 **** --- 220,226 ---- bool date_is_int; bool float8_pass_by_value; bool data_checksum_version; + int file_encryption_keylen; } ControlData; /* *************** typedef struct *** 293,298 **** --- 295,301 ---- int jobs; /* number of processes/threads to use */ char *socketdir; /* directory to use for Unix sockets */ bool ind_coll_unknown; /* mark unknown index collation versions */ + bool pass_terminal_fd; /* pass -R to pg_ctl? */ } UserOpts; typedef struct diff --git a/src/bin/pg_upgrade/server.c b/src/bin/pg_upgrade/server.c new file mode 100644 index 713509f..9208ad0 *** a/src/bin/pg_upgrade/server.c --- b/src/bin/pg_upgrade/server.c *************** start_postmaster(ClusterInfo *cluster, b *** 244,251 **** * vacuumdb --freeze actually freezes the tuples. */ snprintf(cmd, sizeof(cmd), ! "\"%s/pg_ctl\" -w -l \"%s\" -D \"%s\" -o \"-p %d%s%s %s%s\" start", ! cluster->bindir, SERVER_LOG_FILE, cluster->pgconfig, cluster->port, (cluster->controldata.cat_ver >= BINARY_UPGRADE_SERVER_FLAG_CAT_VER) ? " -b" : " -c autovacuum=off -c autovacuum_freeze_max_age=2000000000", --- 244,252 ---- * vacuumdb --freeze actually freezes the tuples. */ snprintf(cmd, sizeof(cmd), ! "\"%s/pg_ctl\" -w%s -l \"%s\" -D \"%s\" -o \"-p %d%s%s %s%s\" start", ! cluster->bindir, user_opts.pass_terminal_fd ? " -R" : "", ! SERVER_LOG_FILE, cluster->pgconfig, cluster->port, (cluster->controldata.cat_ver >= BINARY_UPGRADE_SERVER_FLAG_CAT_VER) ? " -b" : " -c autovacuum=off -c autovacuum_freeze_max_age=2000000000", diff --git a/src/common/Makefile b/src/common/Makefile new file mode 100644 index af891cb..d7d0f7a *** a/src/common/Makefile --- b/src/common/Makefile *************** OBJS_COMMON = \ *** 61,66 **** --- 61,67 ---- ip.o \ jsonapi.o \ keywords.o \ + kmgr_utils.o \ kwlookup.o \ link-canary.o \ md5_common.o \ *************** OBJS_COMMON = \ *** 81,90 **** --- 82,93 ---- ifeq ($(with_openssl),yes) OBJS_COMMON += \ + cipher_openssl.o \ protocol_openssl.o \ cryptohash_openssl.o else OBJS_COMMON += \ + cipher.o \ cryptohash.o \ md5.o \ sha2.o diff --git a/src/common/cipher.c b/src/common/cipher.c new file mode 100644 index ...6a80543 *** a/src/common/cipher.c --- b/src/common/cipher.c *************** *** 0 **** --- 1,70 ---- + /*------------------------------------------------------------------------- + * + * cipher.c + * Shared frontend/backend for cryptographic functions + * + * Copyright (c) 2020, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/common/cipher.c + * + *------------------------------------------------------------------------- + */ + + #ifndef FRONTEND + #include "postgres.h" + #else + #include "postgres_fe.h" + #endif + + #include "common/cipher.h" + + static cipher_failure(void); + + PgCipherCtx * + pg_cipher_ctx_create(int cipher, uint8 *key, int klen) + { + cipher_failure(); + } + + void + pg_cipher_ctx_free(PgCipherCtx *ctx) + { + cipher_failure(); + } + + bool + pg_cipher_encrypt(PgCipherCtx *ctx, const uint8 *in, int inlen, + uint8 *out, int *outlen, const uint8 *iv) + { + cipher_failure(); + } + + bool + pg_cipher_decrypt(PgCipherCtx *ctx, const uint8 *in, int inlen, + uint8 *out, int *outlen, const uint8 *iv) + { + cipher_failure(); + } + + bool + pg_HMAC_SHA512(const uint8 *key, const uint8 *in, int inlen, + uint8 *out) + { + cipher_failure(); + } + + static + cipher_failure(void) + { + #ifndef FRONTEND + ereport(ERROR, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + (errmsg("cluster encryption is not supported because OpenSSL is not supported by this build"), + errhint("Compile with --with-openssl to use cluster encryption.")))); + #else + fprintf(stderr, _("cluster encryption is not supported because OpenSSL is not supported by this build")); + exit(1); + #endif + } + diff --git a/src/common/cipher_openssl.c b/src/common/cipher_openssl.c new file mode 100644 index ...44d1597 *** a/src/common/cipher_openssl.c --- b/src/common/cipher_openssl.c *************** *** 0 **** --- 1,186 ---- + /*------------------------------------------------------------------------- + * cipher_openssl.c + * Cryptographic function using OpenSSL + * + * This contains the common low-level functions needed in both frontend and + * backend, for implement the database encryption. + * + * Portions Copyright (c) 2020, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/common/cipher_openssl.c + * + *------------------------------------------------------------------------- + */ + #ifndef FRONTEND + #include "postgres.h" + #else + #include "postgres_fe.h" + #endif + + #include "common/sha2.h" + + #include "common/cipher.h" + #include + #include + #include + #include + #include + + /* + * prototype for the EVP functions that return an algorithm, e.g. + * EVP_aes_128_cbc(). + */ + typedef const EVP_CIPHER *(*ossl_EVP_cipher_func) (void); + + static ossl_EVP_cipher_func get_evp_aes_cbc(int klen); + static EVP_CIPHER_CTX *ossl_cipher_ctx_create(int cipher, uint8 *key, int klen, + bool enc); + + /* + * Return a newly created cipher context. 'cipher' specifies cipher algorithm + * by identifer like PG_CIPHER_XXX. + */ + PgCipherCtx * + pg_cipher_ctx_create(int cipher, uint8 *key, int klen) + { + PgCipherCtx *ctx = NULL; + + if (cipher >= PG_MAX_CIPHER_ID) + return NULL; + + ctx = (PgCipherCtx *) palloc0(sizeof(PgCipherCtx)); + + ctx->encctx = ossl_cipher_ctx_create(cipher, key, klen, true); + ctx->decctx = ossl_cipher_ctx_create(cipher, key, klen, false); + + return ctx; + } + + void + pg_cipher_ctx_free(PgCipherCtx *ctx) + { + EVP_CIPHER_CTX_free(ctx->encctx); + EVP_CIPHER_CTX_free(ctx->decctx); + } + + bool + pg_cipher_encrypt(PgCipherCtx *ctx, const uint8 *in, int inlen, + uint8 *out, int *outlen, const uint8 *iv) + { + int len; + int enclen; + + if (!EVP_EncryptInit_ex(ctx->encctx, NULL, NULL, NULL, iv)) + return false; + + if (!EVP_EncryptUpdate(ctx->encctx, out, &len, in, inlen)) + return false; + + enclen = len; + + if (!EVP_EncryptFinal_ex(ctx->encctx, (uint8 *) ((char *) out + enclen), + &len)) + return false; + + *outlen = enclen + len; + + return true; + } + + bool + pg_cipher_decrypt(PgCipherCtx *ctx, const uint8 *in, int inlen, + uint8 *out, int *outlen, const uint8 *iv) + { + int declen; + int len; + + if (!EVP_DecryptInit_ex(ctx->decctx, NULL, NULL, NULL, iv)) + return false; + + if (!EVP_DecryptUpdate(ctx->decctx, out, &len, in, inlen)) + return false; + + declen = len; + + if (!EVP_DecryptFinal_ex(ctx->decctx, (uint8 *) ((char *) out + declen), + &len)) + return false; + + *outlen = declen + len; + + return true; + } + + bool + pg_HMAC_SHA512(const uint8 *key, const uint8 *in, int inlen, + uint8 *out) + { + return HMAC(EVP_sha512(), key, PG_SHA512_DIGEST_LENGTH, + in, (uint32) inlen, out, NULL); + } + + static ossl_EVP_cipher_func + get_evp_aes_cbc(int klen) + { + switch (klen) + { + case PG_AES128_KEY_LEN: + return EVP_aes_128_cbc; + case PG_AES192_KEY_LEN: + return EVP_aes_192_cbc; + case PG_AES256_KEY_LEN: + return EVP_aes_256_cbc; + default: + return NULL; + } + } + + /* + * Initialize and return an EVP_CIPHER_CTX. Return NULL if the given + * cipher algorithm is not supported or on failure.. + */ + static EVP_CIPHER_CTX * + ossl_cipher_ctx_create(int cipher, uint8 *key, int klen, bool enc) + { + EVP_CIPHER_CTX *ctx; + ossl_EVP_cipher_func func; + int ret; + + ctx = EVP_CIPHER_CTX_new(); + + switch (cipher) + { + case PG_CIPHER_AES_CBC: + func = get_evp_aes_cbc(klen); + if (!func) + goto failed; + break; + default: + goto failed; + } + + if (enc) + ret = EVP_EncryptInit_ex(ctx, (const EVP_CIPHER *) func(), NULL, key, NULL); + else + ret = EVP_DecryptInit_ex(ctx, (const EVP_CIPHER *) func(), NULL, key, NULL); + + if (!ret) + goto failed; + + if (!EVP_CIPHER_CTX_set_key_length(ctx, PG_AES256_KEY_LEN)) + goto failed; + + /* + * Always enable padding. We don't need to check the return value as + * EVP_CIPHER_CTX_set_padding always returns 1. + */ + EVP_CIPHER_CTX_set_padding(ctx, 1); + + return ctx; + + failed: + EVP_CIPHER_CTX_free(ctx); + return NULL; + } + diff --git a/src/common/cryptohash_openssl.c b/src/common/cryptohash_openssl.c new file mode 100644 index 118651c..b0b283e *** a/src/common/cryptohash_openssl.c --- b/src/common/cryptohash_openssl.c *************** *** 25,30 **** --- 25,31 ---- #include "common/cryptohash.h" #ifndef FRONTEND + #include "miscadmin.h" #include "utils/memutils.h" #include "utils/resowner.h" #include "utils/resowner_private.h" *************** pg_cryptohash_create(pg_cryptohash_type *** 85,91 **** ctx->type = type; #ifndef FRONTEND ! ResourceOwnerEnlargeCryptoHash(CurrentResourceOwner); #endif /* --- 86,97 ---- ctx->type = type; #ifndef FRONTEND ! /* ! * Allow cluster file encryption to use this in bootstrap mode and ! * early in server start. ! */ ! if (CurrentResourceOwner) ! ResourceOwnerEnlargeCryptoHash(CurrentResourceOwner); #endif /* *************** pg_cryptohash_create(pg_cryptohash_type *** 109,117 **** } #ifndef FRONTEND ! state->resowner = CurrentResourceOwner; ! ResourceOwnerRememberCryptoHash(CurrentResourceOwner, ! PointerGetDatum(ctx)); #endif return ctx; --- 115,128 ---- } #ifndef FRONTEND ! if (CurrentResourceOwner) ! { ! state->resowner = CurrentResourceOwner; ! ResourceOwnerRememberCryptoHash(CurrentResourceOwner, ! PointerGetDatum(ctx)); ! } ! else ! state->resowner = NULL; #endif return ctx; *************** pg_cryptohash_free(pg_cryptohash_ctx *ct *** 221,228 **** EVP_MD_CTX_destroy(state->evpctx); #ifndef FRONTEND ! ResourceOwnerForgetCryptoHash(state->resowner, ! PointerGetDatum(ctx)); #endif explicit_bzero(state, sizeof(pg_cryptohash_state)); --- 232,244 ---- EVP_MD_CTX_destroy(state->evpctx); #ifndef FRONTEND ! /* ! * Allow cluster file encryption to use this in bootstrap mode and ! * early in server start. ! */ ! if (state->resowner) ! ResourceOwnerForgetCryptoHash(state->resowner, ! PointerGetDatum(ctx)); #endif explicit_bzero(state, sizeof(pg_cryptohash_state)); diff --git a/src/common/kmgr_utils.c b/src/common/kmgr_utils.c new file mode 100644 index ...9e77dc5 *** a/src/common/kmgr_utils.c --- b/src/common/kmgr_utils.c *************** *** 0 **** --- 1,580 ---- + /*------------------------------------------------------------------------- + * + * 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 "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 cluster passphrase: " + + #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); + + /* Return a key wrap context initialized with the given keys */ + PgKeyWrapCtx * + pg_create_keywrap_ctx(uint8 key[KMGR_ENC_KEY_LEN], uint8 mackey[KMGR_MAC_KEY_LEN]) + { + PgKeyWrapCtx *ctx; + + ctx = (PgKeyWrapCtx *) palloc0(sizeof(PgKeyWrapCtx)); + + /* Create and initialize a cipher context */ + ctx->cipherctx = pg_cipher_ctx_create(PG_CIPHER_AES_CBC, key, KMGR_ENC_KEY_LEN); + if (ctx->cipherctx == NULL) + return NULL; + + /* Set encryption key and MAC key */ + memcpy(ctx->mackey, mackey, KMGR_MAC_KEY_LEN); + + return ctx; + } + + /* Free the key wrap context */ + void + pg_free_keywrap_ctx(PgKeyWrapCtx *ctx) + { + if (!ctx) + return; + + Assert(ctx->cipherctx); + + pg_cipher_ctx_free(ctx->cipherctx); + + #ifndef FRONTEND + pfree(ctx); + #else + pg_free(ctx); + #endif + } + + /* + * 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(PgKeyWrapCtx *ctx, CryptoKey *in, CryptoKey *out) + { + uint8 *hmac; + uint8 *iv; + uint8 *enc; + int enclen; + + Assert(ctx && in && out); + + hmac = out->key; + iv = hmac + KMGR_HMAC_LEN; + enc = iv + PG_AES_IV_SIZE; + + /* Generate IV */ + if (!pg_strong_random(iv, PG_AES_IV_SIZE)) + return false; + + if (!pg_cipher_encrypt(ctx->cipherctx, in->key, in->klen, enc, &enclen, iv)) + return false; + + if (!pg_HMAC_SHA512(ctx->mackey, enc, enclen, hmac)) + return false; + + out->klen = KmgrSizeOfCipherText(in->klen);; + Assert(out->klen == KMGR_HMAC_LEN + PG_AES_IV_SIZE + enclen); + + 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(PgKeyWrapCtx *ctx, CryptoKey *in, CryptoKey *out) + { + uint8 hmac[KMGR_HMAC_LEN]; + uint8 *expected_hmac; + uint8 *iv; + uint8 *enc; + int enclen; + + Assert(ctx && in && out); + + expected_hmac = in->key; + iv = expected_hmac + KMGR_HMAC_LEN; + enc = iv + PG_AES_IV_SIZE; + enclen = in->klen - (enc - in->key); + + /* Verify the correctness of HMAC */ + if (!pg_HMAC_SHA512(ctx->mackey, enc, enclen, hmac)) + return false; + + if (memcmp(hmac, expected_hmac, KMGR_HMAC_LEN) != 0) + return false; + + /* Decrypt encrypted data */ + if (!pg_cipher_decrypt(ctx->cipherctx, enc, enclen, out->key, &(out->klen), iv)) + return false; + + return true; + } + + /* + * Verify the correctness of the given passphrase by unwrapping the given keys. + * If the given passphrase 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_passphrase(char *passphrase, int passlen, + CryptoKey *in_keys, CryptoKey *out_keys, int nkeys) + { + PgKeyWrapCtx *tmpctx; + uint8 user_enckey[KMGR_ENC_KEY_LEN]; + uint8 user_hmackey[KMGR_MAC_KEY_LEN]; + + /* + * Create temporary wrap context with encryption key and HMAC key extracted + * from the passphrase. + */ + kmgr_derive_keys(passphrase, passlen, user_enckey, user_hmackey); + tmpctx = pg_create_keywrap_ctx(user_enckey, user_hmackey); + + for (int i = 0; i < nkeys; i++) + { + if (!kmgr_unwrap_key(tmpctx, &(in_keys[i]), &(out_keys[i]))) + { + /* The passphrase is not correct */ + pg_free_keywrap_ctx(tmpctx); + return false; + } + explicit_bzero(&(in_keys[i]), sizeof(in_keys[i])); + } + + /* The passphrase is correct, free the cipher context */ + pg_free_keywrap_ctx(tmpctx); + + return true; + } + + /* Generate encryption key and mac key from given passphrase */ + void + kmgr_derive_keys(char *passphrase, Size passlen, + uint8 enckey[KMGR_ENC_KEY_LEN], + uint8 mackey[KMGR_MAC_KEY_LEN]) + { + pg_cryptohash_ctx *ctx1, *ctx2; + + StaticAssertStmt(KMGR_ENC_KEY_LEN == PG_AES256_KEY_LEN, + "derived encryption key size does not match AES256 key size"); + StaticAssertStmt(KMGR_MAC_KEY_LEN == PG_HMAC_SHA512_KEY_LEN, + "derived mac key size does not match HMAC-SHA512 key size"); + + /* Generate encryption key from passphrase */ + ctx1 = pg_cryptohash_create(PG_SHA256); + if (pg_cryptohash_init(ctx1) < 0 || + pg_cryptohash_update(ctx1, (uint8 *) passphrase, passlen) < 0 || + pg_cryptohash_final(ctx1, enckey) < 0) + { + #ifdef FRONTEND + pg_log_fatal("could not create cluster file encryption hash"); + exit(EXIT_FAILURE); + #else + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not create cluster file encryption hash"))); + #endif + } + pg_cryptohash_free(ctx1); + + /* Generate mac key from passphrase */ + ctx2 = pg_cryptohash_create(PG_SHA512); + if (pg_cryptohash_init(ctx2) < 0 || + pg_cryptohash_update(ctx2, (uint8 *) passphrase, passlen) < 0 || + pg_cryptohash_final(ctx2, mackey) < 0) + { + #ifdef FRONTEND + pg_log_fatal("could not create cluster file encryption hash"); + exit(EXIT_FAILURE); + #else + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not create cluster file encryption hash"))); + #endif + } + pg_cryptohash_free(ctx2); + } + + /* + * Run cluster passphrase 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_passphrase_command(char *passphrase_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 = passphrase_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 passphrase referenced %%R, but -R not specified"); + exit(EXIT_FAILURE); + #else + ereport(ERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("cluster passphrase referenced %%R, but -R 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 ((len = fread(buf, sizeof(char), size, fh)) < size) + { + 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 + } + + 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 + } diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h new file mode 100644 index 06bed90..a4c1259 *** a/src/include/catalog/pg_control.h --- b/src/include/catalog/pg_control.h *************** *** 22,28 **** /* Version identifier for this pg_control format */ ! #define PG_CONTROL_VERSION 1300 /* Nonce key length, see below */ #define MOCK_AUTH_NONCE_LEN 32 --- 22,28 ---- /* Version identifier for this pg_control format */ ! #define PG_CONTROL_VERSION 1400 /* Nonce key length, see below */ #define MOCK_AUTH_NONCE_LEN 32 *************** typedef struct ControlFileData *** 226,231 **** --- 226,234 ---- */ char mock_authentication_nonce[MOCK_AUTH_NONCE_LEN]; + /* File encryption key length. Zero if disabled. */ + int file_encryption_keylen; + /* CRC of all above ... MUST BE LAST! */ pg_crc32c crc; } ControlFileData; diff --git a/src/include/common/cipher.h b/src/include/common/cipher.h new file mode 100644 index ...f8bd4db *** a/src/include/common/cipher.h --- b/src/include/common/cipher.h *************** *** 0 **** --- 1,78 ---- + /*------------------------------------------------------------------------- + * + * cipher.h + * Declarations for cryptographic functions + * + * Portions Copyright (c) 2020, PostgreSQL Global Development Group + * + * src/include/common/cipher.h + * + *------------------------------------------------------------------------- + */ + #ifndef PG_CIPHER_H + #define PG_CIPHER_H + + #ifdef USE_OPENSSL + #include + #include + #include + #endif + + /* + * Supported symmetric encryption algorithm. These identifiers are passed + * to pg_cipher_ctx_create() function, and then actual encryption + * implementations need to initialize their context of the given encryption + * algorithm. + */ + #define PG_CIPHER_AES_CBC 0 + #define PG_MAX_CIPHER_ID 1 + + /* AES128/192/256 various length definitions */ + #define PG_AES128_KEY_LEN (128 / 8) + #define PG_AES192_KEY_LEN (192 / 8) + #define PG_AES256_KEY_LEN (256 / 8) + + /* + * The encrypted data is a series of blocks of size. Initialization + * vector(IV) is the same size of cipher block. + */ + #define PG_AES_BLOCK_SIZE 16 + #define PG_AES_IV_SIZE (PG_AES_BLOCK_SIZE) + + /* HMAC key and HMAC length. We use HMAC-SHA256 */ + #define PG_HMAC_SHA512_KEY_LEN 64 + #define PG_HMAC_SHA512_LEN 64 + + #ifdef USE_OPENSSL + typedef EVP_CIPHER_CTX cipher_private_ctx; + #else + typedef void cipher_private_ctx; + #endif + + /* + * This struct has two implementation-private context for + * encryption and decryption. The caller must create the encryption + * context using by pg_cipher_ctx_create() and pass the context to + * pg_cipher_encrypt() or pg_cipher_decrypt(). + */ + typedef struct PgCipherCtx + { + cipher_private_ctx *encctx; + cipher_private_ctx *decctx; + } PgCipherCtx; + + extern PgCipherCtx *pg_cipher_ctx_create(int cipher, uint8 *key, int klen); + extern void pg_cipher_ctx_free(PgCipherCtx *ctx); + extern bool pg_cipher_encrypt(PgCipherCtx *ctx, + const uint8 *in, int inlen, + uint8 *out, int *outlen, + const uint8 *iv); + extern bool pg_cipher_decrypt(PgCipherCtx *ctx, + const uint8 *in, int inlen, + uint8 *out, int *outlen, + const uint8 *iv); + extern bool pg_HMAC_SHA512(const uint8 *key, + const uint8 *in, int inlen, + uint8 *out); + + #endif /* PG_CIPHER_H */ diff --git a/src/include/common/kmgr_utils.h b/src/include/common/kmgr_utils.h new file mode 100644 index ...a25bb7d *** a/src/include/common/kmgr_utils.h --- b/src/include/common/kmgr_utils.h *************** *** 0 **** --- 1,108 ---- + /*------------------------------------------------------------------------- + * + * kmgr_utils.h + * Declarations for utility function for file encryption key + * + * Portions Copyright (c) 2020, PostgreSQL Global Development Group + * + * src/include/common/kmgr_utils.h + * + *------------------------------------------------------------------------- + */ + #ifndef KMGR_UTILS_H + #define KMGR_UTILS_H + + #include "common/cipher.h" + + /* Current version number */ + #define KMGR_VERSION 1 + + /* + * Directories where cluster file encryption keys reside within PGDATA. + */ + #define KMGR_DIR "pg_cryptokeys" + #define KMGR_DIR_PID KMGR_DIR"/pg_altercpass.pid" + #define LIVE_KMGR_DIR KMGR_DIR"/live" + /* used during cluster passphrase rotation */ + #define NEW_KMGR_DIR KMGR_DIR"/new" + #define OLD_KMGR_DIR KMGR_DIR"/old" + + /* + * Identifiers of internal keys. + */ + #define KMGR_REL_KEY_ID 0 + #define KMGR_WAL_KEY_ID 1 + #define KMGR_MAX_INTERNAL_KEYS 2 + + /* Encryption key and MAC key used for key wrapping */ + /* Though the data key length can be shorter, we always encrypt with AES256 */ + #define KMGR_ENC_KEY_LEN PG_AES256_KEY_LEN + #define KMGR_MAC_KEY_LEN PG_HMAC_SHA512_KEY_LEN + #define KMGR_HMAC_LEN PG_HMAC_SHA512_LEN + + /* Key wrapping key consists of encryption key and mac key */ + #define KMGR_KEY_LEN (PG_AEAD_ENC_KEY_LEN + PG_AEAD_MAC_KEY_LEN) + + /* Allowed length of cluster passphrase */ + #define KMGR_MIN_PASSPHRASE_LEN 64 + #define KMGR_MAX_PASSPHRASE_LEN 1024 + + /* Maximum length of key the key manager can store */ + #define KMGR_MAX_KEY_LEN 256 + #define KMGR_MAX_WRAPPED_KEY_LEN KmgrSizeOfCipherText(KMGR_MAX_KEY_LEN) + + /* + * Size of encrypted key size with padding. We use PKCS#7 padding, + * described in RFC 5652. + */ + #define SizeOfDataWithPadding(klen) \ + ((int)(klen) + (PG_AES_BLOCK_SIZE - ((int)(klen) % PG_AES_BLOCK_SIZE))) + + /* Macros to compute the size of cipher text and plain text */ + #define KmgrSizeOfCipherText(len) \ + (KMGR_MAC_KEY_LEN + PG_AES_IV_SIZE + SizeOfDataWithPadding((int)(len))) + #define KmgrSizeOfPlainText(klen) \ + ((int)(klen) - (KMGR_MAC_KEY_LEN + PG_AES_IV_SIZE)) + + /* CryptoKey file name is keys id */ + #define CryptoKeyFilePath(path, dir, id) \ + snprintf((path), MAXPGPATH, "%s/%d", (dir), (id)) + + /* + * Cryptographic key data structure. This structure is used for + * both on-disk (raw key) and on-memory (wrapped key). + */ + typedef struct CryptoKey + { + int klen; + uint8 key[KMGR_MAX_WRAPPED_KEY_LEN]; + } CryptoKey; + + /* Key wrapping cipher context */ + typedef struct PgKeyWrapCtx + { + uint8 mackey[KMGR_MAC_KEY_LEN]; + PgCipherCtx *cipherctx; + } PgKeyWrapCtx; + + extern PgKeyWrapCtx *pg_create_keywrap_ctx(uint8 key[KMGR_ENC_KEY_LEN], + uint8 mackey[KMGR_MAC_KEY_LEN]); + extern void pg_free_keywrap_ctx(PgKeyWrapCtx *ctx); + extern bool kmgr_wrap_key(PgKeyWrapCtx *ctx, CryptoKey *in, CryptoKey *out); + extern bool kmgr_unwrap_key(PgKeyWrapCtx *ctx, CryptoKey *in, CryptoKey *out); + + + + extern void kmgr_derive_keys(char *passphrase, Size passlen, + uint8 enckey[KMGR_ENC_KEY_LEN], + uint8 mackey[KMGR_MAC_KEY_LEN]); + extern bool kmgr_verify_passphrase(char *passphrase, int passlen, + CryptoKey *in_keys, CryptoKey *out_keys, + int nkey); + extern bool kmgr_wrap_key(PgKeyWrapCtx *ctx, CryptoKey *in, CryptoKey *out); + extern bool kmgr_unwrap_key(PgKeyWrapCtx *ctx, CryptoKey *in, CryptoKey *out); + extern int kmgr_run_cluster_passphrase_command(char *passphrase_command, + char *buf, int size, char *dir); + extern CryptoKey *kmgr_get_cryptokeys(const char *path, int *nkeys); + + #endif /* KMGR_UTILS_H */ diff --git a/src/include/crypto/kmgr.h b/src/include/crypto/kmgr.h new file mode 100644 index ...5464af1 *** a/src/include/crypto/kmgr.h --- b/src/include/crypto/kmgr.h *************** *** 0 **** --- 1,29 ---- + /*------------------------------------------------------------------------- + * + * kmgr.h + * + * Portions Copyright (c) 2020, PostgreSQL Global Development Group + * + * src/include/crypto/kmgr.h + * + *------------------------------------------------------------------------- + */ + #ifndef KMGR_H + #define KMGR_H + + #include "common/cipher.h" + #include "common/kmgr_utils.h" + #include "storage/relfilenode.h" + #include "storage/bufpage.h" + + /* GUC parameters */ + extern int file_encryption_keylen; + extern char *cluster_passphrase_command; + + extern Size KmgrShmemSize(void); + extern void KmgrShmemInit(void); + extern void BootStrapKmgr(void); + extern void InitializeKmgr(void); + extern const CryptoKey *KmgrGetKey(int id); + + #endif /* KMGR_H */ diff --git a/src/include/pgstat.h b/src/include/pgstat.h new file mode 100644 index 5954068..b8f98f9 *** a/src/include/pgstat.h --- b/src/include/pgstat.h *************** typedef enum *** 1010,1015 **** --- 1010,1018 ---- WAIT_EVENT_DATA_FILE_TRUNCATE, WAIT_EVENT_DATA_FILE_WRITE, WAIT_EVENT_DSM_FILL_ZERO_WRITE, + WAIT_EVENT_KEY_FILE_READ, + WAIT_EVENT_KEY_FILE_WRITE, + WAIT_EVENT_KEY_FILE_SYNC, WAIT_EVENT_LOCK_FILE_ADDTODATADIR_READ, WAIT_EVENT_LOCK_FILE_ADDTODATADIR_SYNC, WAIT_EVENT_LOCK_FILE_ADDTODATADIR_WRITE, diff --git a/src/include/postmaster/postmaster.h b/src/include/postmaster/postmaster.h new file mode 100644 index babc87d..b1f0721 *** a/src/include/postmaster/postmaster.h --- b/src/include/postmaster/postmaster.h *************** extern bool enable_bonjour; *** 30,35 **** --- 30,37 ---- extern char *bonjour_name; extern bool restart_after_crash; + extern int terminal_fd; + #ifdef WIN32 extern HANDLE PostmasterHandle; #else diff --git a/src/include/utils/guc_tables.h b/src/include/utils/guc_tables.h new file mode 100644 index 7f36e11..c0dbf69 *** a/src/include/utils/guc_tables.h --- b/src/include/utils/guc_tables.h *************** enum config_group *** 89,94 **** --- 89,95 ---- STATS, STATS_MONITORING, STATS_COLLECTOR, + ENCRYPTION, AUTOVACUUM, CLIENT_CONN, CLIENT_CONN_STATEMENT, diff --git a/src/test/Makefile b/src/test/Makefile new file mode 100644 index 14cde4f..7db4c0b *** a/src/test/Makefile --- b/src/test/Makefile *************** endif *** 30,36 **** endif ifeq ($(with_openssl),yes) ifneq (,$(filter ssl,$(PG_TEST_EXTRA))) ! SUBDIRS += ssl endif endif --- 30,36 ---- endif ifeq ($(with_openssl),yes) ifneq (,$(filter ssl,$(PG_TEST_EXTRA))) ! SUBDIRS += ssl crypto endif endif diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm new file mode 100644 index f92c140..c7a08ad *** a/src/tools/msvc/Mkvcbuild.pm --- b/src/tools/msvc/Mkvcbuild.pm *************** sub mkvcbuild *** 122,139 **** archive.c base64.c checksum_helper.c config_info.c controldata_utils.c d2s.c encnames.c exec.c f2s.c file_perm.c file_utils.c hashfn.c ip.c jsonapi.c ! keywords.c kwlookup.c link-canary.c md5_common.c pg_get_line.c pg_lzcompress.c pgfnames.c psprintf.c relpath.c rmtree.c saslprep.c scram-common.c string.c stringinfo.c unicode_norm.c username.c wait_error.c wchar.c); if ($solution->{options}->{openssl}) { push(@pgcommonallfiles, 'cryptohash_openssl.c'); push(@pgcommonallfiles, 'protocol_openssl.c'); } else { push(@pgcommonallfiles, 'cryptohash.c'); push(@pgcommonallfiles, 'md5.c'); push(@pgcommonallfiles, 'sha2.c'); --- 122,141 ---- archive.c base64.c checksum_helper.c config_info.c controldata_utils.c d2s.c encnames.c exec.c f2s.c file_perm.c file_utils.c hashfn.c ip.c jsonapi.c ! keywords.c kmgr_utils.c kwlookup.c link-canary.c md5_common.c pg_get_line.c pg_lzcompress.c pgfnames.c psprintf.c relpath.c rmtree.c saslprep.c scram-common.c string.c stringinfo.c unicode_norm.c username.c wait_error.c wchar.c); if ($solution->{options}->{openssl}) { + push(@pgcommonallfiles, 'cipher_openssl.c'); push(@pgcommonallfiles, 'cryptohash_openssl.c'); push(@pgcommonallfiles, 'protocol_openssl.c'); } else { + push(@pgcommonallfiles, 'cipher.c'); push(@pgcommonallfiles, 'cryptohash.c'); push(@pgcommonallfiles, 'md5.c'); push(@pgcommonallfiles, 'sha2.c');