From 9ab9a26b865717aaa7575afe7e771bdb63da12eb Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Wed, 29 Jun 2022 00:01:22 +0100 Subject: [PATCH v2] Transparent column encryption --- doc/src/sgml/acronyms.sgml | 18 + doc/src/sgml/catalogs.sgml | 275 +++++++ doc/src/sgml/datatype.sgml | 47 ++ doc/src/sgml/ddl.sgml | 73 ++ doc/src/sgml/glossary.sgml | 23 + doc/src/sgml/libpq.sgml | 257 +++++- doc/src/sgml/protocol.sgml | 346 ++++++++ doc/src/sgml/ref/create_table.sgml | 42 +- doc/src/sgml/ref/psql-ref.sgml | 33 + src/test/Makefile | 3 +- src/test/column_encryption/.gitignore | 3 + src/test/column_encryption/Makefile | 29 + .../t/001_column_encryption.pl | 206 +++++ .../column_encryption/t/002_cmk_rotation.pl | 144 ++++ src/test/column_encryption/test_client.c | 210 +++++ .../column_encryption/test_run_decrypt.pl | 41 + .../traces/disallowed_in_pipeline.trace | 2 +- .../traces/multi_pipelines.trace | 4 +- .../libpq_pipeline/traces/nosync.trace | 20 +- .../traces/pipeline_abort.trace | 4 +- .../libpq_pipeline/traces/prepared.trace | 6 +- .../traces/simple_pipeline.trace | 2 +- .../libpq_pipeline/traces/singlerow.trace | 6 +- .../libpq_pipeline/traces/transaction.trace | 2 +- .../regress/expected/column_encryption.out | 59 ++ src/test/regress/expected/oidjoins.out | 6 + src/test/regress/expected/opr_sanity.out | 12 +- src/test/regress/expected/prepare.out | 41 +- src/test/regress/expected/psql.out | 25 + src/test/regress/expected/rules.out | 6 +- src/test/regress/expected/type_sanity.out | 20 +- src/test/regress/parallel_schedule | 3 + src/test/regress/pg_regress_main.c | 2 +- src/test/regress/sql/column_encryption.sql | 50 ++ src/test/regress/sql/prepare.sql | 6 +- src/test/regress/sql/psql.sql | 17 + src/test/regress/sql/type_sanity.sql | 4 +- src/include/access/printtup.h | 2 + src/include/catalog/pg_amop.dat | 5 + src/include/catalog/pg_amproc.dat | 5 + src/include/catalog/pg_attribute.h | 9 + src/include/catalog/pg_cast.dat | 6 + src/include/catalog/pg_colenckey.h | 55 ++ src/include/catalog/pg_colenckeydata.h | 46 ++ src/include/catalog/pg_colmasterkey.h | 45 ++ src/include/catalog/pg_opclass.dat | 2 + src/include/catalog/pg_operator.dat | 10 + src/include/catalog/pg_opfamily.dat | 2 + src/include/catalog/pg_proc.dat | 13 +- src/include/catalog/pg_type.dat | 12 + src/include/catalog/pg_type.h | 1 + src/include/nodes/parsenodes.h | 1 + src/include/parser/analyze.h | 4 +- src/include/parser/parse_node.h | 2 + src/include/parser/parse_param.h | 3 +- src/include/tcop/tcopprot.h | 2 + src/include/utils/plancache.h | 4 + src/include/utils/syscache.h | 3 + src/backend/access/common/printsimple.c | 2 + src/backend/access/common/printtup.c | 137 ++++ src/backend/access/common/tupdesc.c | 12 + src/backend/access/hash/hashvalidate.c | 2 +- src/backend/catalog/Makefile | 3 +- src/backend/catalog/heap.c | 3 + src/backend/commands/prepare.c | 59 +- src/backend/commands/tablecmds.c | 74 ++ src/backend/executor/spi.c | 4 + src/backend/nodes/copyfuncs.c | 1 + src/backend/nodes/equalfuncs.c | 1 + src/backend/nodes/nodeFuncs.c | 2 + src/backend/nodes/outfuncs.c | 1 + src/backend/parser/analyze.c | 3 +- src/backend/parser/gram.y | 13 +- src/backend/parser/parse_param.c | 43 +- src/backend/parser/parse_target.c | 6 + src/backend/replication/basebackup_copy.c | 12 + src/backend/replication/walsender.c | 5 + src/backend/tcop/postgres.c | 54 +- src/backend/utils/cache/plancache.c | 10 + src/backend/utils/cache/syscache.c | 29 + src/bin/pg_dump/pg_dump.c | 26 +- src/bin/pg_dump/pg_dump.h | 1 + src/bin/psql/command.c | 31 + src/bin/psql/common.c | 38 +- src/bin/psql/describe.c | 29 +- src/bin/psql/help.c | 3 + src/bin/psql/settings.h | 5 + src/bin/psql/startup.c | 10 + src/bin/psql/tab-complete.c | 2 +- src/interfaces/libpq/Makefile | 1 + src/interfaces/libpq/exports.txt | 4 + src/interfaces/libpq/fe-connect.c | 20 + src/interfaces/libpq/fe-encrypt-openssl.c | 753 ++++++++++++++++++ src/interfaces/libpq/fe-encrypt.h | 33 + src/interfaces/libpq/fe-exec.c | 594 +++++++++++++- src/interfaces/libpq/fe-protocol3.c | 123 ++- src/interfaces/libpq/fe-trace.c | 7 + src/interfaces/libpq/libpq-fe.h | 22 + src/interfaces/libpq/libpq-int.h | 34 + src/interfaces/libpq/nls.mk | 2 +- src/interfaces/libpq/test/.gitignore | 1 + src/interfaces/libpq/test/Makefile | 7 + .../postgres_fdw/expected/postgres_fdw.out | 2 +- 103 files changed, 4380 insertions(+), 123 deletions(-) create mode 100644 src/test/column_encryption/.gitignore create mode 100644 src/test/column_encryption/Makefile create mode 100644 src/test/column_encryption/t/001_column_encryption.pl create mode 100644 src/test/column_encryption/t/002_cmk_rotation.pl create mode 100644 src/test/column_encryption/test_client.c create mode 100755 src/test/column_encryption/test_run_decrypt.pl create mode 100644 src/test/regress/expected/column_encryption.out create mode 100644 src/test/regress/sql/column_encryption.sql create mode 100644 src/include/catalog/pg_colenckey.h create mode 100644 src/include/catalog/pg_colenckeydata.h create mode 100644 src/include/catalog/pg_colmasterkey.h create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c create mode 100644 src/interfaces/libpq/fe-encrypt.h diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml index 9ed148ab84..e21c9bcce3 100644 --- a/doc/src/sgml/acronyms.sgml +++ b/doc/src/sgml/acronyms.sgml @@ -56,6 +56,15 @@ Acronyms + + CEK + + + Column Encryption Key + + + + CIDR @@ -67,6 +76,15 @@ Acronyms + + CMK + + + Column Master Key + + + + CPAN diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 25b02c4e37..cedaf55c07 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -105,6 +105,21 @@ System Catalogs collations (locale information) + + pg_colenckey + column encryption keys + + + + pg_colenckeydata + column encryption key data + + + + pg_colmasterkey + column master keys + + pg_constraint check constraints, unique constraints, primary key constraints, foreign key constraints @@ -1360,6 +1375,40 @@ <structname>pg_attribute</structname> Columns + + + attcek oid + (references pg_colenckey.oid) + + + If the column is encrypted, a reference to the column encryption key, else 0. + + + + + + attrealtypid oid + (references pg_type.oid) + + + If the column is encrypted, then this column indicates the type of the + encrypted data that is reported to the client. For encrypted columns, + the field atttypid is either + pg_encrypted_det or pg_encrypted_rnd. If the + column is not encrypted, then 0. + + + + + + attencalg int16 + + + If the column is encrypted, the identifier of the encryption algorithm; + see for possible values. + + + attinhcount int4 @@ -2437,6 +2486,232 @@ <structname>pg_collation</structname> Columns + + <structname>pg_colenckey</structname> + + + pg_colenckey + + + + The catalog pg_colenckey contains information + about the column encryption keys in the database. The actual key material + of the column encryption keys is in the catalog pg_colenckeydata. + + + + <structname>pg_colenckey</structname> Columns + + + + + Column Type + + + Description + + + + + + + + oid oid + + + Row identifier + + + + + + cekname name + + + Column encryption key name + + + + + + cekowner oid + (references pg_authid.oid) + + + Owner of the column encryption key + + + + +
+
+ + + <structname>pg_colenckeydata</structname> + + + pg_colenckeydata + + + + The catalog pg_colenckeydata contains the key + material of column encryption keys. Each column encryption key object can + contain several versions of the key material, each encrypted with a + different column master key. That allows the gradual rotation of the + column master keys. Thus, (ckdcekid, ckdcmkid) is a + unique key of this table. + + + + The key material of column encryption keys should never be decrypted inside + the database instance. It is meant to be sent as-is to the client, where + it is decrypted using the associated column master key, and then used to + encrypt or decrypt column values. + + + + <structname>pg_colenckeydata</structname> Columns + + + + + Column Type + + + Description + + + + + + + + oid oid + + + Row identifier + + + + + + ckdcekid oid + (references pg_colenckey.oid) + + + The column encryption key this entry belongs to + + + + + + ckdcmkid oid + (references pg_colmasterkey.oid) + + + The column master key that the key material is encrypted with + + + + + + ckdcmkalg int16 + + + The encryption algorithm used for encrypting the key material; see + for possible values. + + + + + + ckdencval bytea + + + The key material of this column encryption key, encrypted using the + referenced column master key + + + + +
+
+ + + <structname>pg_colmasterkey</structname> + + + pg_colmasterkey + + + + The catalog pg_colmasterkey contains information + about column master keys. The keys themselves are not stored in the + database. The catalog entry only contains information that is used by + clients to locate the keys, for example in a file or in a key management + system. + + + + <structname>pg_colmasterkey</structname> Columns + + + + + Column Type + + + Description + + + + + + + + oid oid + + + Row identifier + + + + + + cmkname name + + + Column master key name + + + + + + cmkowner oid + (references pg_authid.oid) + + + Owner of the column master key + + + + + + cmkrealm text + + + A realm associated with this column master key. This is + a freely chosen string that is used by clients to determine how to look + up the key. A typical configuration would put all CMKs that are looked + up in the same way into the same realm. + + + + +
+
+ <structname>pg_constraint</structname> diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml index 8e30b82273..cdd0530eeb 100644 --- a/doc/src/sgml/datatype.sgml +++ b/doc/src/sgml/datatype.sgml @@ -5342,4 +5342,51 @@ Pseudo-Types + + Types Related to Encryption + + + An encrypted column value (see ) is + internally stored using the types + pg_encrypted_rnd (for randomized encryption) or + pg_encrypted_det (for deterministic encryption); see . Most of the database system treats + this as normal types. For example, the type pg_encrypted_det has + an equals operator that allows lookup of encrypted values. The external + representation of these types is the same as the bytea type. + Thus, clients that don't support transparent column encryption or have + disabled it will see the encrypted values as byte arrays. Clients that + support transparent data encryption will not see these types in result + sets, as the protocol layer will translate them back to declared + underlying type in the table definition. + + + + Types Related to Encryption + + + + + + + Name + Storage Size + Description + + + + + pg_encrypted_det + 1 or 4 bytes plus the actual binary string + encrypted column value, deterministic encryption + + + pg_encrypted_rnd + 1 or 4 bytes plus the actual binary string + encrypted column value, randomized encryption + + + +
+
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml index b01e3ad544..bb8503584e 100644 --- a/doc/src/sgml/ddl.sgml +++ b/doc/src/sgml/ddl.sgml @@ -1211,6 +1211,79 @@ Exclusion Constraints + + Transparent Column Encryption + + + With transparent column encryption, columns can be + stored encrypted in the database. The encryption and decryption happens on + the client, so that the plaintext value is never seen in the database + instance or on the server hosting the database. The drawback is that most + operations, such as function calls or sorting, are not possible on + encrypted values. + + + + Tranparent column encryption uses two levels of cryptographic keys. The + actual column value is encrypted using a symmetric algorithm, such as AES, + using a column encryption key + (CEK). The column encryption key is in turn encrypted + using an assymmetric algorithm, such as RSA, using a column + master key (CMK). The encrypted CEK is + stored in the database system. The CMK is not stored in the database + system; it is stored on the client or somewhere where the client can access + it, such as in a local file or in a key management system. The database + system only records where the CMK is stored and provides this information + to the client. When rows containing encrypted columns are sent to the + client, the server first sends any necessary CMK information, followed by + any required CEK. The client then looks up the CMK and uses that to + decrypt the CEK. Then it decrypts incoming row data using the CEK and + provides the decrypted row data to the application. + + + + Here is an example declaring a column as encrypted: + +CREATE TABLE customers ( + id int PRIMARY KEY, + name text NOT NULL, + ... + creditcard_num text ENCRYPTED WITH (column_encryption_key = cek1) +); + + + + + Column encryption supports randomized + (also known as probabilistic) and + deterministic encryption. The above example uses + randomized encryption, which is the default. Randomized encryption uses a + random initialization vector for each encryption, so that even if the + plaintext of two rows is equal, the encrypted values will be different. + This prevents someone with direct access to the database server to make + computations such as distinct counts on the encrypted values. + Deterministic encryption uses a fixed initialization vector. This reduces + security, but it allows equality searches on encrypted values. The + following example declares a column with deterministic encryption: + +CREATE TABLE employees ( + id int PRIMARY KEY, + name text NOT NULL, + ... + ssn text ENCRYPTED WITH ( + column_encryption_key = cek1, encryption_type = deterministic) +); + + + + + Null values are not encrypted by transparent column encryption; null values + sent by the client are visible as null values in the database. If the fact + that a value is null needs to be hidden from the server, this information + needs to be encoded into a nonnull value in the client somehow. + + + System Columns diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml index d6d0a3a814..10507e4391 100644 --- a/doc/src/sgml/glossary.sgml +++ b/doc/src/sgml/glossary.sgml @@ -353,6 +353,29 @@ Glossary + + Column encryption key + + + A cryptographic key used to encrypt column values when using transparent + column encryption. Column encryption keys are stored in the database + encrypted by another key, the column master key. + + + + + + Column master key + + + A cryptographic key used to encrypt column encryption keys. (So the + column master key is a key encryption key.) + Column master keys are stored outside the database system, for example in + a key management system. + + + + Commit diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 37ec3cb4e5..d135d882eb 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -1974,6 +1974,110 @@ Parameter Key Words
+ + + cmklookup + + + This specifies how libpq should look up column master keys (CMKs) in + order to decrypt the column encryption keys (CEKs). + The value is a list of key=value entries separated + by semicolons. Each key is the name of a key realm, or + * to match all realms. The value is a + scheme:data specification. The scheme specifies + the method to look up the key, the remaining data is specific to the + scheme. Placeholders are replaced in the remaining data as follows: + + + + %a + + + The CMK algorithm name (see ) + + + + + + %b + + + The encrypted CEK data in Base64 encoding (only for the run scheme) + + + + + + %k + + + The CMK key name + + + + + + %r + + + The realm name + + + + + + + + Available schemes are: + + + file + + + Load the key material from a file. The remaining data is the file + name. Use this if the CMKs are kept in a file on the file system. + + + + + + run + + + Run the specified command to decrypt the CEK. The remaining data + is a shell command. Use this with key management systems that + perform the decryption themselves. The command must print the + decrypted plaintext in Base64 format on the standard output. + + + + + + + + The default value is empty. + + + + Example: + +cmklookup="r1=file:/some/where/secrets/%k.pem;*=file:/else/where/%r/%k.pem" + + This specification says, for keys in realm r1, load + them from the specified file, replacing %k by the + key name. For keys in other realms, load them from the file, + replacing realm and key names as specified. + + + + An example for interacting with a (hypothetical) key management + system: + +cmklookup="*=run:acmekms decrypt --key %k --alg %a --blob '%b'" + + + + @@ -3022,6 +3126,57 @@ Main Functions + + PQexecPrepared2PQexecPrepared2 + + + + Sends a request to execute a prepared statement with given + parameters, and waits for the result, with support for encrypted columns. + +PGresult *PQexecPrepared2(PGconn *conn, + const char *stmtName, + int nParams, + const char * const *paramValues, + const int *paramLengths, + const int *paramFormats, + const int *paramForceColumnEncryptions, + int resultFormat, + PGresult *paramDesc); + + + + + is like with additional support for encrypted + columns. The parameter paramDesc must be a + result set obtained from on + the same prepared statement. + + + + This function must be used if a statement parameter corresponds to an + underlying encrypted column. In that situation, the prepared + statement needs to be described first so that libpq can obtain the + necessary key and other information from the server. When that is + done, the parameters corresponding to encrypted columns are + automatically encrypted appropriately before being sent to the server. + + + + The parameter paramForceColumnEncryptions + specifies whether encryption should be forced for a parameter. If + encryption is forced for a parameter but it does not correspond to an + encrypted column on the server, then the call will fail and the + parameter will not be sent. This can be used for additional security + against a comprimised server. (The drawback is that application code + then needs to be kept up to date with knowledge about which columns + are encrypted rather than letting the server specify this.) If the + array pointer is null then encryption is not forced for any parameter. + + + + PQdescribePreparedPQdescribePrepared @@ -3869,6 +4024,28 @@ Retrieving Query Result Information + + PQfisencryptedPQfisencrypted + + + + Returns whether the value for the given column came from an encrypted + column. Column numbers start at 0. + +int PQfisencrypted(const PGresult *res, + int column_number); + + + + + Encrypted column values are automatically decrypted, so this function + is not necessary to access the column value. It can be used for extra + security to check whether the value was stored encrypted when one + thought it should be. + + + + PQfsizePQfsize @@ -4044,12 +4221,37 @@ Retrieving Query Result Information This function is only useful when inspecting the result of - . For other types of queries it + . For other types of results it will return zero. + + PQparamisencryptedPQparamisencrypted + + + + Returns whether the value for the given parameter is destined for an + encrypted column. Parameter numbers start at 0. + +int PQparamisencrypted(const PGresult *res, int param_number); + + + + + Values for parameters destined for encrypted columns are automatically + encrypted, so this function is not necessary to prepare the parameter + value. It can be used for extra security to check whether the value + will be stored encrypted when one thought it should be. (But see also + at for another way to do that.) + This function is only useful when inspecting the result of . For other types of results it + will return false. + + + + PQprintPQprint @@ -4575,6 +4777,7 @@ Asynchronous Command Processing , , , + , , and , which can be used with to duplicate @@ -4582,6 +4785,7 @@ Asynchronous Command Processing , , , + , , and respectively. @@ -4693,6 +4897,46 @@ Asynchronous Command Processing + + PQsendQueryPrepared2PQsendQueryPrepared2 + + + + Sends a request to execute a prepared statement with given + parameters, without waiting for the result(s), with support for encrypted columns. + +int PQsendQueryPrepared2(PGconn *conn, + const char *stmtName, + int nParams, + const char * const *paramValues, + const int *paramLengths, + const int *paramFormats, + const int *paramForceColumnEncryptions, + int resultFormat, + PGresult *paramDesc); + + + + + is like with additional support for encrypted + columns. The parameter paramDesc must be a + result set obtained from on + the same prepared statement. + + + + This function must be used if a statement parameter corresponds to an + underlying encrypted column. In that situation, the prepared + statement needs to be described first so that libpq can obtain the + necessary key and other information from the server. When that is + done, the parameters corresponding to encrypted columns are + automatically encrypted appropriately before being sent to the server. + See also under . + + + + PQsendDescribePreparedPQsendDescribePrepared @@ -4743,6 +4987,7 @@ Asynchronous Command Processing , , , + , , , or @@ -7771,6 +8016,16 @@ Environment Variables + + + + PGCMKLOOKUP + + PGCMKLOOKUP behaves the same as the connection parameter. + + + diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index a94743b587..3bc3d30fd8 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -1050,6 +1050,52 @@ Extended Query + + Transparent Column Encryption + + + When transparent column encryption is enabled, the messages + ColumnMasterKey and ColumnEncryptionKey can appear before RowDescription + and ParameterDescription messages. Clients should collect the information + in these messages and keep them for the duration of the connection. A + server is not required to resend the key information for each statement + cycle if it was already sent during this connection. If a server resends + a key that the client has already stored (that is, a key having an ID + equal to one already stored), the new information should replace the old. + (This could happen, for example, if the key was altered by server-side DDL + commands.) + + + + A client supporting transparent column encryption should automatically + decrypt the column value fields of DataRow messages corresponding to + encrypted columns, and it should automatically encrypt the parameter value + fields of Bind messages corresponding to encrypted columns. + + + + When column encryption is used, the plaintext is always in text format + (not binary format). + + + + When deterministic encryption is used, clients need to take care to + represent plaintext to be encrypted in a consistent form. For example, + encrypting an integer represented by the string 100 and + an integer represented by the string +100 would result + in two different ciphertexts, thus defeating the main point of + deterministic encryption. This protocol specification requires the + plaintext to be in canonical form, which is the form that + is produced by the server when it outputs a particular value in text + format. + + + + The cryptographic operations used for transparent column encryption are + described in . + + + Function Call @@ -3991,6 +4037,128 @@ Message Formats + + ColumnEncryptionKey (B) + + + + Byte1('Y') + + + Identifies the message as a column encryption key message. + + + + + + Int32 + + + Length of message contents in bytes, including self. + + + + + + Int32 + + + The session-specific identifier of the key. + + + + + + Int32 + + + The identifier of the master key used to encrypt this key. + + + + + + Int16 + + + The identifier of the algorithm used to encrypt this key. + + + + + + Int32 + + + The length of the following key material. + + + + + + Byten + + + The key material, encrypted with the master key referenced above. + + + + + + + + + ColumnMasterKey (B) + + + + Byte1('y') + + + Identifies the message as a column master key message. + + + + + + Int32 + + + Length of message contents in bytes, including self. + + + + + + Int32 + + + The session-specific identifier of the key. + + + + + + String + + + The name of the key. + + + + + + String + + + The key's realm. + + + + + + + CommandComplete (B) @@ -5086,6 +5254,37 @@ Message Formats + + + Int32 + + + If this parameter is to be encrypted, this specifies the + identifier of the column encryption key to use, else zero. + + + + + + Int16 + + + If this parameter is to be encrypted, this specifies the + identifier of the encryption algorithm, else zero. + + + + + + Int16 + + + This is used as a bit field of flags. If the column is + encrypted and bit 0x01 is set, the column uses deterministic + encryption, otherwise randomized encryption. + + + @@ -5474,6 +5673,26 @@ Message Formats + + + Int32 + + + If this parameter is to be encrypted, this specifies the + identifier of the column encryption key to use, else zero. + + + + + + Int16 + + + If this parameter is to be encrypted, this specifies the + identifier of the encrypt algorithm, else zero. + + + @@ -7278,6 +7497,133 @@ Logical Replication Message Formats + + Transparent Column Encryption Cryptography + + + This section describes the cryptographic operations used by transparent + column encryption. A client that supports transparent column encryption + needs to implement these operations as specified here in order to be able + to interoperate with other clients. + + + + Column encryption key algorithms and column master key algorithms are + identified by integers in the protocol messages and the system catalogs. + Additional algorithms may be added to this protocol specification without a + change in the protocol version number. Clients should implement support + for all the algorithms specified here. If a client encounters an algorithm + identifier it does not recognize or does not support, it must raise an + error. A suitable error message should be provided to the application or + user. + + + + Column Master Keys + + + The currently defined algorithms for column master keys are listed in + . + + + + Column Master Key Algorithms + + + + ID + Name + Reference + + + + + 1 + RSAES_OAEP_SHA_1 + RFC 8017 (PKCS #1) + + + 2 + RSAES_OAEP_SHA_256 + RFC 8017 (PKCS #1) + + + +
+
+ + + Column Encryption Keys + + + The currently defined algorithms for column encryption keys are listed in + . + + + + Column Encryption Key Algorithms + + + + ID + Name + Reference + Key length (octets) + + + + + 130 + AEAD_AES_128_CBC_HMAC_SHA_256 + + 32 + + + 131 + AEAD_AES_192_CBC_HMAC_SHA_384 + + 48 + + + 132 + AEAD_AES_256_CBC_HMAC_SHA_384 + + 56 + + + 133 + AEAD_AES_256_CBC_HMAC_SHA_512 + + 64 + + + +
+ + + The associated data in these algorithms consists of 4 + bytes: The ASCII letters P and G + (byte values 80 and 71), followed by the algorithm ID as a 16-bit unsigned + integer in network byte order. + + + + The length of the initialization vector is 16 octets for all CEK algorithm + variants. For randomized encryption, the initialization vector should be + (cryptographically strong) random bytes. For deterministic encryption, + the initialization vector is constructed as + +SUBSTRING(HMAC(K, P) FOR IVLEN) + + where HMAC is the HMAC function associated with + the algorithm, K is the prefix of the CEK of + the length required by the HMAC function, and P + is the plaintext to be encrypted. (This is the same portion of the CEK + that the AEAD_AES_*_HMAC_* algorithms use for the MAC part.) + +
+
+ Summary of Changes since Protocol 2.0 diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index 6c9918b0a1..7a1d03f868 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -22,7 +22,7 @@ CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name ( [ - { column_name data_type [ COMPRESSION compression_method ] [ COLLATE collation ] [ column_constraint [ ... ] ] + { column_name data_type [ COMPRESSION compression_method ] [ ENCRYPTED WITH ( encryption_options ) ] [ COLLATE collation ] [ column_constraint [ ... ] ] | table_constraint | LIKE source_table [ like_option ... ] } [, ... ] @@ -322,6 +322,46 @@ Parameters + + ENCRYPTED WITH ( encryption_options ) + + + Enables transparent column encryption for the column. + encryption_options are comma-separated + key=value specifications. The following options are + available: + + + column_encryption_key + + + Specifies the name of the column encryption key to use. Specifying + this is mandatory. + + + + + encryption_type + + + randomized (the default) or deterministic + + + + + algorithm + + + The encryption algorithm to use. The default is + AEAD_AES_128_CBC_HMAC_SHA_256. + + + + + + + + INHERITS ( parent_table [, ... ] ) diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 65bb0a6a3f..e6c93f1b13 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -2255,6 +2255,28 @@ Meta-Commands + + \gencr [ parameter ] ... + + + + Sends the current query buffer to the server for execution, as with + \g, with the specified parameters passed for any + parameter placeholders ($1 etc.). This command + ensures that any parameters corresponding to encrypted columns are + sent to the server encrypted. + + + + Example: + +INSERT INTO tbl1 VALUES ($1, $2) \gencr 'first value' 'second value' + + + + + + \getenv psql_var env_var @@ -3945,6 +3967,17 @@ Variables + + HIDE_COLUMN_ENCRYPTION + + + If this variable is set to true, column encryption + details are not displayed. This is mainly useful for regression + tests. + + + + HIDE_TOAST_COMPRESSION diff --git a/src/test/Makefile b/src/test/Makefile index 69ef074d75..2300be8332 100644 --- a/src/test/Makefile +++ b/src/test/Makefile @@ -32,6 +32,7 @@ SUBDIRS += ldap endif endif ifeq ($(with_ssl),openssl) +SUBDIRS += column_encryption ifneq (,$(filter ssl,$(PG_TEST_EXTRA))) SUBDIRS += ssl endif @@ -41,7 +42,7 @@ endif # clean" etc to recurse into them. (We must filter out those that we # have conditionally included into SUBDIRS above, else there will be # make confusion.) -ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl) +ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl column_encryption) # We want to recurse to all subdirs for all standard targets, except that # installcheck and install should not recurse into the subdirectory "modules". diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore new file mode 100644 index 0000000000..456dbf69d2 --- /dev/null +++ b/src/test/column_encryption/.gitignore @@ -0,0 +1,3 @@ +/test_client +# Generated by test suite +/tmp_check/ diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile new file mode 100644 index 0000000000..7aca84b17a --- /dev/null +++ b/src/test/column_encryption/Makefile @@ -0,0 +1,29 @@ +#------------------------------------------------------------------------- +# +# Makefile for src/test/column_encryption +# +# Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/test/column_encryption/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/test/column_encryption +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS) +LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport) + +all: test_client + +check: all + $(prove_check) + +installcheck: + $(prove_installcheck) + +clean distclean maintainer-clean: + rm -f test_client.o test_client + rm -rf tmp_check diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl new file mode 100644 index 0000000000..7ab98981d8 --- /dev/null +++ b/src/test/column_encryption/t/001_column_encryption.pl @@ -0,0 +1,206 @@ +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->start; + + +sub create_cmk +{ + my ($cmkname) = @_; + + my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem"; + + system_or_bail 'openssl', 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename; + + $node->safe_psql('postgres', qq{ +INSERT INTO pg_colmasterkey (oid, cmkname, cmkowner, cmkrealm) VALUES ( + pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'), + '${cmkname}', + (select oid from pg_roles where rolname = current_user), + '' +); +}); + + return $cmkfilename; +} + +sub create_cek +{ + my ($cekname, $bytes, $cmkname, $cmkfilename) = @_; + + # generate random bytes + system_or_bail 'openssl', 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes; + + # encrypt CEK using CMK + system_or_bail 'openssl', 'pkeyutl', '-encrypt', + '-inkey', $cmkfilename, + '-pkeyopt', 'rsa_padding_mode:oaep', + '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", + '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"; + + my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"); + + # create CEK in database + $node->safe_psql('postgres', qq{ +INSERT INTO pg_colenckey (oid, cekname, cekowner) VALUES ( + pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'), + '${cekname}', + (select oid from pg_roles where rolname = current_user) +); +}); + $node->safe_psql('postgres', qq{ +INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES ( + pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'), + (SELECT oid FROM pg_colenckey WHERE cekname = '${cekname}'), + (SELECT oid FROM pg_colmasterkey WHERE cmkname = '${cmkname}'), + 1, + '\\x${cekenchex}' +); +}); + + return; +} + + +my $cmk1filename = create_cmk('cmk1'); +my $cmk2filename = create_cmk('cmk2'); +create_cek('cek1', 32, 'cmk1', $cmk1filename); +create_cek('cek2', 48, 'cmk2', $cmk2filename); + +$ENV{'PGCMKLOOKUP'} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem'; + + +$node->safe_psql('postgres', qq{ +CREATE TABLE tbl1 ( + a int, + b text ENCRYPTED WITH (column_encryption_key = cek1) +); +}); + +$node->safe_psql('postgres', qq{ +CREATE TABLE tbl2 ( + a int, + b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1) +); +}); + +$node->safe_psql('postgres', qq{ +CREATE TABLE tbl3 ( + a int, + b text ENCRYPTED WITH (column_encryption_key = cek1), + c text ENCRYPTED WITH (column_encryption_key = cek2, algorithm = 'AEAD_AES_192_CBC_HMAC_SHA_384') +); +}); + +$node->safe_psql('postgres', q{ +INSERT INTO tbl1 (a, b) VALUES (1, $1) \gencr 'val1' +INSERT INTO tbl1 (a, b) VALUES (2, $1) \gencr 'val2' +}); + +# Expected ciphertext length is 2 blocks of AES output (2 * 16) plus +# half SHA-256 output (16) in hex encoding: (2 * 16 + 16) * 2 = 96 +like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1) TO STDOUT}), + qr/1\t\\\\x[0-9a-f]{96}\n2\t\\\\x[0-9a-f]{96}/, + 'inserted data is encrypted'); + +my $result; + +$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1 \gdesc}); +is($result, + q(a|integer +b|text), + 'query result description has original type'); + +$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1}); +is($result, + q(1|val1 +2|val2), + 'decrypted query result'); + +{ + local $ENV{'PGCMKLOOKUP'} = '*=run:broken %k "%b"'; + $result = $node->psql('postgres', q{SELECT a, b FROM tbl1}); + isnt($result, 0, 'query fails with broken cmklookup run setting'); +} + +{ + local $ENV{'PGCMKLOOKUP'} = "*=run:perl ./test_run_decrypt.pl '${PostgreSQL::Test::Utils::tmp_check}' %k %a '%b'"; + + $result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1}); + is($result, + q(1|val1 +2|val2), + 'decrypted query result with cmklookup run'); +} + +$node->safe_psql('postgres', q{ +INSERT INTO tbl3 (a, b, c) VALUES (1, $1, $2) \gencr 'valB1' 'valC1' +}); + +$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3}); +is($result, + q(1|valB1|valC1), + 'decrypted query result multiple keys'); + + +$node->command_fails_like(['test_client', 'test1'], qr/not encrypted/, 'test client fails because parameters not encrypted'); + +$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1}); +is($result, + q(1|val1 +2|val2), + 'decrypted query result after test client insert'); + +$node->command_ok(['test_client', 'test2'], 'test client test 2'); + +$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1}); +is($result, + q(1|val1 +2|val2 +3|val3), + 'decrypted query result after test client insert 2'); + +like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1 WHERE a = 3) TO STDOUT}), + qr/3\t\\\\x[0-9a-f]{96}/, + 'inserted data is encrypted'); + +$node->command_ok(['test_client', 'test3'], 'test client test 3'); + +$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1}); +is($result, + q(1|val1 +2|val2 +3|val3upd), + 'decrypted query result after test client insert 3'); + +$node->command_ok(['test_client', 'test4'], 'test client test 4'); + +$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2}); +is($result, + q(1|valA +2|valB +3|valA), + 'decrypted query result after test client insert 4'); + +is($node->safe_psql('postgres', q{SELECT b, count(*) FROM tbl2 GROUP BY b ORDER BY 2}), + q(valB|1 +valA|2), + 'group by deterministically encrypted column'); + +$node->command_ok(['test_client', 'test5'], 'test client test 5'); + +$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3}); +is($result, + q(1|valB1|valC1 +2|valB2|valC2 +3|valB3|valC3), + 'decrypted query result multiple keys after test client insert 5'); + +done_testing(); diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl new file mode 100644 index 0000000000..5a13a206eb --- /dev/null +++ b/src/test/column_encryption/t/002_cmk_rotation.pl @@ -0,0 +1,144 @@ +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +# Test column master key rotation. First, we generate CMK1 and a CEK +# encrypted with it. Then we add a CMK2 and encrypt the CEK with it +# as well. (Recall that a CEK can be associated with multiple CMKs, +# for this reason. That's why pg_colenckeydata is split out from +# pg_colenckey.) Then we remove CMK1. We test that we can get +# decrypted query results at each step. + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init; +$node->start; + + +sub create_cmk +{ + my ($cmkname) = @_; + + my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem"; + + system_or_bail 'openssl', 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename; + + $node->safe_psql('postgres', qq{ +INSERT INTO pg_colmasterkey (oid, cmkname, cmkowner, cmkrealm) VALUES ( + pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'), + '${cmkname}', + (select oid from pg_roles where rolname = current_user), + '' +); +}); + + return $cmkfilename; +} + + +my $cmk1filename = create_cmk('cmk1'); + +# create CEK +my ($cekname, $bytes) = ('cek1', 16+16); + +# generate random bytes +system_or_bail 'openssl', 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes; + +# encrypt CEK using CMK +system_or_bail 'openssl', 'pkeyutl', '-encrypt', + '-inkey', $cmk1filename, + '-pkeyopt', 'rsa_padding_mode:oaep', + '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", + '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"; + +my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"); + +# create CEK in database +$node->safe_psql('postgres', qq{ +INSERT INTO pg_colenckey (oid, cekname, cekowner) VALUES ( + pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'), + '${cekname}', + (select oid from pg_roles where rolname = current_user) +); +}); + +$node->safe_psql('postgres', qq{ +INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES ( + pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'), + (SELECT oid FROM pg_colenckey WHERE cekname = '${cekname}'), + (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'), + 1, + '\\x${cekenchex}' +); +}); + +$ENV{'PGCMKLOOKUP'} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem'; + +$node->safe_psql('postgres', qq{ +CREATE TABLE tbl1 ( + a int, + b text ENCRYPTED WITH (column_encryption_key = cek1) +); +}); + +$node->safe_psql('postgres', q{ +INSERT INTO tbl1 (a, b) VALUES (1, $1) \gencr 'val1' +INSERT INTO tbl1 (a, b) VALUES (2, $1) \gencr 'val2' +}); + +is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}), + q(1|val1 +2|val2), + 'decrypted query result with one CMK'); + + +# create new CMK +my $cmk2filename = create_cmk('cmk2'); + +# encrypt CEK using new CMK +# +# (Here, we still have the plaintext of the CEK available from +# earlier. In reality, one would decrypt the CEK with the first CMK +# and then re-encrypt it with the second CMK.) +system_or_bail 'openssl', 'pkeyutl', '-encrypt', + '-inkey', $cmk2filename, + '-pkeyopt', 'rsa_padding_mode:oaep', + '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", + '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"; + +$cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc"); + +# add new data record for CEK in database +$node->safe_psql('postgres', qq{ +INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES ( + pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'), + (SELECT oid FROM pg_colenckey WHERE cekname = '${cekname}'), + (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk2'), + 1, + '\\x${cekenchex}' +); +}); + + +is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}), + q(1|val1 +2|val2), + 'decrypted query result with two CMKs'); + + +# delete CEK record for first CMK +$node->safe_psql('postgres', qq{ +DELETE FROM pg_colenckeydata WHERE ckdcmkid = (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'); +}); + + +is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}), + q(1|val1 +2|val2), + 'decrypted query result with only new CMK'); + + +done_testing(); diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c new file mode 100644 index 0000000000..a64774bb3c --- /dev/null +++ b/src/test/column_encryption/test_client.c @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2021-2022, PostgreSQL Global Development Group + */ + +#include "postgres_fe.h" + +#include "libpq-fe.h" + + +static int +test1(PGconn *conn) +{ + PGresult *res; + const char *values[] = {"3", "val3"}; + + res = PQexecParams(conn, "INSERT INTO tbl1 (a, b) VALUES ($1, $2)", + 2, NULL, values, NULL, NULL, 0); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQexecParams() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + return 0; +} + +static int +test2(PGconn *conn) +{ + PGresult *res, + *res2; + const char *values[] = {"3", "val3"}; + int forces[] = {false, true}; + + res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b) VALUES ($1, $2)", + 2, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQprepare() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + res2 = PQdescribePrepared(conn, ""); + if (PQresultStatus(res2) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQdescribePrepared() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + if (!(!PQparamisencrypted(res2, 0) && + PQparamisencrypted(res2, 1))) + { + fprintf(stderr, "wrong results from PQparamisencrypted()\n"); + return 1; + } + + res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, forces, 0, res2); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQexecPrepared() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + return 0; +} + +static int +test3(PGconn *conn) +{ + PGresult *res, + *res2; + const char *values[] = {"3", "val3upd"}; + + res = PQprepare(conn, "", "UPDATE tbl1 SET b = $2 WHERE a = $1", + 2, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQprepare() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + res2 = PQdescribePrepared(conn, ""); + if (PQresultStatus(res2) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQdescribePrepared() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, NULL, 0, res2); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQexecPrepared() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + return 0; +} + +static int +test4(PGconn *conn) +{ + PGresult *res, + *res2; + const char *values[] = {"1", "valA", "2", "valB", "3", "valA"}; + + res = PQprepare(conn, "", "INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6)", + 6, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQprepare() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + res2 = PQdescribePrepared(conn, ""); + if (PQresultStatus(res2) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQdescribePrepared() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + res = PQexecPrepared2(conn, "", 6, values, NULL, NULL, NULL, 0, res2); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQexecPrepared() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + return 0; +} + +static int +test5(PGconn *conn) +{ + PGresult *res, + *res2; + const char *values[] = { + "2", "valB2", "valC2", + "3", "valB3", "valC3" + }; + + res = PQprepare(conn, "", "INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6)", + 6, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQprepare() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + res2 = PQdescribePrepared(conn, ""); + if (PQresultStatus(res2) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQdescribePrepared() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + res = PQexecPrepared2(conn, "", 6, values, NULL, NULL, NULL, 0, res2); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + { + fprintf(stderr, "PQexecPrepared() failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + return 0; +} + +int +main(int argc, char **argv) +{ + PGconn *conn; + int ret = 0; + + conn = PQconnectdb(""); + if (PQstatus(conn) != CONNECTION_OK) + { + fprintf(stderr, "Connection to database failed: %s\n", + PQerrorMessage(conn)); + return 1; + } + + if (argc < 2 || argv[1] == NULL) + return 87; + else if (strcmp(argv[1], "test1") == 0) + ret = test1(conn); + else if (strcmp(argv[1], "test2") == 0) + ret = test2(conn); + else if (strcmp(argv[1], "test3") == 0) + ret = test3(conn); + else if (strcmp(argv[1], "test4") == 0) + ret = test4(conn); + else if (strcmp(argv[1], "test5") == 0) + ret = test5(conn); + else + ret = 88; + + PQfinish(conn); + return ret; +} diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl new file mode 100755 index 0000000000..9664349f14 --- /dev/null +++ b/src/test/column_encryption/test_run_decrypt.pl @@ -0,0 +1,41 @@ +#!/usr/bin/perl + +# Test/sample command for libpq cmklookup run scheme +# +# This just places the data into temporary files and runs the openssl +# command on it. (In practice, this could more simply be written as a +# shell script, but this way it's more portable.) + +# Copyright (c) 2021-2022, PostgreSQL Global Development Group + +use strict; +use warnings; + +use MIME::Base64; + +my ($tmpdir, $cmkname, $alg, $b64data) = @ARGV; + +die unless $alg eq 'RSAES_OAEP_SHA_1'; + +open my $fh, '>:raw', "${tmpdir}/input.tmp" or die; +print $fh decode_base64($b64data); +close $fh; + +system('openssl', 'pkeyutl', '-decrypt', + '-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep', + '-in', "${tmpdir}/input.tmp", '-out', "${tmpdir}/output.tmp") == 0 or die; + +open $fh, '<:raw', "${tmpdir}/output.tmp" or die; +my $data = ''; + +while (1) { + my $success = read $fh, $data, 100, length($data); + die $! if not defined $success; + last if not $success; +} + +close $fh; + +unlink "${tmpdir}/input.tmp", "${tmpdir}/output.tmp"; + +print encode_base64($data); diff --git a/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace b/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace index dd6df03f1e..5fc9c0346d 100644 --- a/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace +++ b/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace @@ -1,5 +1,5 @@ F 13 Query "SELECT 1" -B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 39 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0 B 11 DataRow 1 1 '1' B 13 CommandComplete "SELECT 1" B 5 ReadyForQuery I diff --git a/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace b/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace index 4b9ab07ca4..700b0b6519 100644 --- a/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace +++ b/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace @@ -10,13 +10,13 @@ F 9 Execute "" 0 F 4 Sync B 4 ParseComplete B 4 BindComplete -B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 39 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0 B 11 DataRow 1 1 '1' B 13 CommandComplete "SELECT 1" B 5 ReadyForQuery I B 4 ParseComplete B 4 BindComplete -B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 39 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0 B 11 DataRow 1 1 '1' B 13 CommandComplete "SELECT 1" B 5 ReadyForQuery I diff --git a/src/test/modules/libpq_pipeline/traces/nosync.trace b/src/test/modules/libpq_pipeline/traces/nosync.trace index d99aac649d..7b1dfa1c15 100644 --- a/src/test/modules/libpq_pipeline/traces/nosync.trace +++ b/src/test/modules/libpq_pipeline/traces/nosync.trace @@ -41,52 +41,52 @@ F 9 Execute "" 0 F 4 Flush B 4 ParseComplete B 4 BindComplete -B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 37 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0 B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' B 13 CommandComplete "SELECT 1" B 4 ParseComplete B 4 BindComplete -B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 37 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0 B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' B 13 CommandComplete "SELECT 1" B 4 ParseComplete B 4 BindComplete -B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 37 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0 B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' B 13 CommandComplete "SELECT 1" B 4 ParseComplete B 4 BindComplete -B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 37 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0 B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' B 13 CommandComplete "SELECT 1" B 4 ParseComplete B 4 BindComplete -B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 37 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0 B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' B 13 CommandComplete "SELECT 1" B 4 ParseComplete B 4 BindComplete -B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 37 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0 B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' B 13 CommandComplete "SELECT 1" B 4 ParseComplete B 4 BindComplete -B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 37 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0 B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' B 13 CommandComplete "SELECT 1" B 4 ParseComplete B 4 BindComplete -B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 37 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0 B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' B 13 CommandComplete "SELECT 1" B 4 ParseComplete B 4 BindComplete -B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 37 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0 B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' B 13 CommandComplete "SELECT 1" B 4 ParseComplete B 4 BindComplete -B 31 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 +B 37 RowDescription 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0 B 70 DataRow 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz' B 13 CommandComplete "SELECT 1" F 4 Terminate diff --git a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace index 3fce548b99..6e4309cadd 100644 --- a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace +++ b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace @@ -50,14 +50,14 @@ F 6 Close P "" F 4 Sync B 4 ParseComplete B 4 BindComplete -B 33 RowDescription 1 "?column?" NNNN 0 NNNN 65535 -1 0 +B 39 RowDescription 1 "?column?" NNNN 0 NNNN 65535 -1 0 NNNN 0 B 32 DataRow 1 22 '0.33333333333333333333' B 32 DataRow 1 22 '0.50000000000000000000' B 32 DataRow 1 22 '1.00000000000000000000' B NN ErrorResponse S "ERROR" V "ERROR" C "22012" M "division by zero" F "SSSS" L "SSSS" R "SSSS" \x00 B 5 ReadyForQuery I F 40 Query "SELECT itemno FROM pq_pipeline_demo" -B 31 RowDescription 1 "itemno" NNNN 2 NNNN 4 -1 0 +B 37 RowDescription 1 "itemno" NNNN 2 NNNN 4 -1 0 NNNN 0 B 11 DataRow 1 1 '3' B 13 CommandComplete "SELECT 1" B 5 ReadyForQuery I diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace index 1a7de5c3e6..8d45bbc0ce 100644 --- a/src/test/modules/libpq_pipeline/traces/prepared.trace +++ b/src/test/modules/libpq_pipeline/traces/prepared.trace @@ -2,8 +2,8 @@ F 68 Parse "select_one" "SELECT $1, '42', $1::numeric, interval '1 sec'" 1 NNNN F 16 Describe S "select_one" F 4 Sync B 4 ParseComplete -B 10 ParameterDescription 1 NNNN -B 113 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 65535 -1 0 "numeric" NNNN 0 NNNN 65535 -1 0 "interval" NNNN 0 NNNN 16 -1 0 +B 18 ParameterDescription 1 NNNN NNNN 0 0 +B 137 RowDescription 4 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0 "?column?" NNNN 0 NNNN 65535 -1 0 NNNN 0 "numeric" NNNN 0 NNNN 65535 -1 0 NNNN 0 "interval" NNNN 0 NNNN 16 -1 0 NNNN 0 B 5 ReadyForQuery I F 10 Query "BEGIN" B 10 CommandComplete "BEGIN" @@ -13,6 +13,6 @@ B 19 CommandComplete "DECLARE CURSOR" B 5 ReadyForQuery T F 16 Describe P "cursor_one" F 4 Sync -B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 39 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0 B 5 ReadyForQuery T F 4 Terminate diff --git a/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace b/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace index 5c94749bc1..5f4849425d 100644 --- a/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace +++ b/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace @@ -5,7 +5,7 @@ F 9 Execute "" 0 F 4 Sync B 4 ParseComplete B 4 BindComplete -B 33 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 +B 39 RowDescription 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0 B 11 DataRow 1 1 '1' B 13 CommandComplete "SELECT 1" B 5 ReadyForQuery I diff --git a/src/test/modules/libpq_pipeline/traces/singlerow.trace b/src/test/modules/libpq_pipeline/traces/singlerow.trace index 9de99befcc..199df4d4f4 100644 --- a/src/test/modules/libpq_pipeline/traces/singlerow.trace +++ b/src/test/modules/libpq_pipeline/traces/singlerow.trace @@ -13,14 +13,14 @@ F 9 Execute "" 0 F 4 Sync B 4 ParseComplete B 4 BindComplete -B 40 RowDescription 1 "generate_series" NNNN 0 NNNN 4 -1 0 +B 46 RowDescription 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0 B 12 DataRow 1 2 '42' B 12 DataRow 1 2 '43' B 12 DataRow 1 2 '44' B 13 CommandComplete "SELECT 3" B 4 ParseComplete B 4 BindComplete -B 40 RowDescription 1 "generate_series" NNNN 0 NNNN 4 -1 0 +B 46 RowDescription 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0 B 12 DataRow 1 2 '42' B 12 DataRow 1 2 '43' B 12 DataRow 1 2 '44' @@ -28,7 +28,7 @@ B 12 DataRow 1 2 '45' B 13 CommandComplete "SELECT 4" B 4 ParseComplete B 4 BindComplete -B 40 RowDescription 1 "generate_series" NNNN 0 NNNN 4 -1 0 +B 46 RowDescription 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0 B 12 DataRow 1 2 '42' B 12 DataRow 1 2 '43' B 12 DataRow 1 2 '44' diff --git a/src/test/modules/libpq_pipeline/traces/transaction.trace b/src/test/modules/libpq_pipeline/traces/transaction.trace index 1dcc2373c0..a6869c4a5b 100644 --- a/src/test/modules/libpq_pipeline/traces/transaction.trace +++ b/src/test/modules/libpq_pipeline/traces/transaction.trace @@ -54,7 +54,7 @@ B 15 CommandComplete "INSERT 0 1" B 5 ReadyForQuery I B 5 ReadyForQuery I F 34 Query "SELECT * FROM pq_pipeline_tst" -B 27 RowDescription 1 "id" NNNN 1 NNNN 4 -1 0 +B 33 RowDescription 1 "id" NNNN 1 NNNN 4 -1 0 NNNN 0 B 11 DataRow 1 1 '3' B 13 CommandComplete "SELECT 1" B 5 ReadyForQuery I diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out new file mode 100644 index 0000000000..f8db4e17ff --- /dev/null +++ b/src/test/regress/expected/column_encryption.out @@ -0,0 +1,59 @@ +\set HIDE_COLUMN_ENCRYPTION false +/* Imagine: +CREATE COLUMN MASTER KEY cmk1 ( + realm = 'test' +); +*/ +INSERT INTO pg_colmasterkey (oid, cmkname, cmkowner, cmkrealm) VALUES ( + pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'), + 'cmk1', + (select oid from pg_roles where rolname = current_user), + 'test' +); +/* Imagine: +CREATE COLUMN ENCRYPTION KEY cek1 ( + column_master_key = cmk1, + algorithm = '...', + encrypted_value = '...' +); +*/ +INSERT INTO pg_colenckey (oid, cekname, cekowner) VALUES ( + pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'), + 'cek1', + (select oid from pg_roles where rolname = current_user) +); +INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES ( + pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'), + (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'), + (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'), + 1, + '\xDEADBEEF' +); +CREATE TABLE tbl_fail ( + a int, + b text, + c text ENCRYPTED WITH (column_encryption_key = notexist) +); +ERROR: column encryption key "notexist" does not exist +CREATE TABLE tbl_29f3 ( + a int, + b text, + c text ENCRYPTED WITH (column_encryption_key = cek1) +); +\d tbl_29f3 + Table "public.tbl_29f3" + Column | Type | Collation | Nullable | Default +--------+---------+-----------+----------+--------- + a | integer | | | + b | text | | | + c | text | | | + +\d+ tbl_29f3 + Table "public.tbl_29f3" + Column | Type | Collation | Nullable | Default | Storage | Encryption | Stats target | Description +--------+---------+-----------+----------+---------+----------+------------+--------------+------------- + a | integer | | | | plain | | | + b | text | | | | extended | | | + c | text | | | | extended | cek1 | | + +DROP TABLE tbl_29f3; -- FIXME: needs pg_dump support for pg_upgrade tests diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be..2aa0e16323 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -73,6 +73,8 @@ NOTICE: checking pg_type {typbasetype} => pg_type {oid} NOTICE: checking pg_type {typcollation} => pg_collation {oid} NOTICE: checking pg_attribute {attrelid} => pg_class {oid} NOTICE: checking pg_attribute {atttypid} => pg_type {oid} +NOTICE: checking pg_attribute {attcek} => pg_colenckey {oid} +NOTICE: checking pg_attribute {attrealtypid} => pg_type {oid} NOTICE: checking pg_attribute {attcollation} => pg_collation {oid} NOTICE: checking pg_class {relnamespace} => pg_namespace {oid} NOTICE: checking pg_class {reltype} => pg_type {oid} @@ -266,3 +268,7 @@ NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} +NOTICE: checking pg_colmasterkey {cmkowner} => pg_authid {oid} +NOTICE: checking pg_colenckey {cekowner} => pg_authid {oid} +NOTICE: checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid} +NOTICE: checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid} diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out index 86d755aa44..4a366074da 100644 --- a/src/test/regress/expected/opr_sanity.out +++ b/src/test/regress/expected/opr_sanity.out @@ -175,13 +175,14 @@ WHERE p1.oid != p2.oid AND ORDER BY 1, 2; proargtypes | proargtypes -----------------------------+-------------------------- + bytea | pg_encrypted_det bigint | xid8 text | character text | character varying timestamp without time zone | timestamp with time zone bit | bit varying txid_snapshot | pg_snapshot -(6 rows) +(7 rows) SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype FROM pg_proc AS p1, pg_proc AS p2 @@ -197,12 +198,13 @@ WHERE p1.oid != p2.oid AND ORDER BY 1, 2; proargtypes | proargtypes -----------------------------+-------------------------- + bytea | pg_encrypted_det integer | xid timestamp without time zone | timestamp with time zone bit | bit varying txid_snapshot | pg_snapshot anyrange | anymultirange -(5 rows) +(6 rows) SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype FROM pg_proc AS p1, pg_proc AS p2 @@ -841,6 +843,8 @@ xid8ge(xid8,xid8) xid8eq(xid8,xid8) xid8ne(xid8,xid8) xid8cmp(xid8,xid8) +pg_encrypted_det_eq(pg_encrypted_det,pg_encrypted_det) +pg_encrypted_det_ne(pg_encrypted_det,pg_encrypted_det) -- restore normal output mode \a\t -- List of functions used by libpq's fe-lobj.c @@ -988,7 +992,9 @@ WHERE c.castmethod = 'b' AND xml | text | 0 | a xml | character varying | 0 | a xml | character | 0 | a -(10 rows) + bytea | pg_encrypted_det | 0 | e + bytea | pg_encrypted_rnd | 0 | e +(12 rows) -- **************** pg_conversion **************** -- Look for illegal values in pg_conversion fields. diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out index 3306c696b1..675556c7d1 100644 --- a/src/test/regress/expected/prepare.out +++ b/src/test/regress/expected/prepare.out @@ -159,25 +159,30 @@ PREPARE q6 AS SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2; PREPARE q7(unknown) AS SELECT * FROM road WHERE thepath = $1; -SELECT name, statement, parameter_types FROM pg_prepared_statements +-- DML statements +PREPARE q8 AS + UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1; +SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns FROM pg_prepared_statements ORDER BY name; - name | statement | parameter_types -------+------------------------------------------------------------------+---------------------------------------------------- - q2 | PREPARE q2(text) AS +| {text} - | SELECT datname, datistemplate, datallowconn +| - | FROM pg_database WHERE datname = $1; | - q3 | PREPARE q3(text, int, float, boolean, smallint) AS +| {text,integer,"double precision",boolean,smallint} - | SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+| - | ten = $3::bigint OR true = $4 OR odd = $5::int) +| - | ORDER BY unique1; | - q5 | PREPARE q5(int, text) AS +| {integer,text} - | SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +| - | ORDER BY unique1; | - q6 | PREPARE q6 AS +| {integer,name} - | SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2; | - q7 | PREPARE q7(unknown) AS +| {path} - | SELECT * FROM road WHERE thepath = $1; | -(5 rows) + name | statement | parameter_types | parameter_orig_tables | parameter_orig_columns +------+------------------------------------------------------------------+----------------------------------------------------+-----------------------+------------------------ + q2 | PREPARE q2(text) AS +| {text} | {-} | {0} + | SELECT datname, datistemplate, datallowconn +| | | + | FROM pg_database WHERE datname = $1; | | | + q3 | PREPARE q3(text, int, float, boolean, smallint) AS +| {text,integer,"double precision",boolean,smallint} | {-,-,-,-,-} | {0,0,0,0,0} + | SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+| | | + | ten = $3::bigint OR true = $4 OR odd = $5::int) +| | | + | ORDER BY unique1; | | | + q5 | PREPARE q5(int, text) AS +| {integer,text} | {-,-} | {0,0} + | SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +| | | + | ORDER BY unique1; | | | + q6 | PREPARE q6 AS +| {integer,name} | {-,-} | {0,0} + | SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2; | | | + q7 | PREPARE q7(unknown) AS +| {path} | {-} | {0} + | SELECT * FROM road WHERE thepath = $1; | | | + q8 | PREPARE q8 AS +| {integer,name} | {-,tenk1} | {0,14} + | UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1; | | | +(6 rows) -- test DEALLOCATE ALL; DEALLOCATE ALL; diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 60acbd1241..33956f6993 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -237,6 +237,31 @@ SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g 3 | Hello | 4 | t (1 row) +-- \gencr +-- (This just tests the parameter passing; there is no encryption here.) +CREATE TABLE test_gencr (a int, b text); +INSERT INTO test_gencr VALUES (1, 'one') \gencr +SELECT * FROM test_gencr WHERE a = 1 \gencr + a | b +---+----- + 1 | one +(1 row) + +INSERT INTO test_gencr VALUES ($1, $2) \gencr 2 'two' +SELECT * FROM test_gencr WHERE a IN ($1, $2) \gencr 2 3 + a | b +---+----- + 2 | two +(1 row) + +-- test parse error +SELECT * FROM test_gencr WHERE a = x \gencr +ERROR: column "x" does not exist +LINE 1: SELECT * FROM test_gencr WHERE a = x + ^ +-- test bind error +SELECT * FROM test_gencr WHERE a = $1 \gencr +ERROR: bind message supplies 0 parameters, but prepared statement "" requires 1 -- \gexec create temporary table gexec_test(a int, b text, c date, d float); select format('create index on gexec_test(%I)', attname) diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index fc3cde3226..898765e255 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1425,8 +1425,10 @@ pg_prepared_statements| SELECT p.name, p.parameter_types, p.from_sql, p.generic_plans, - p.custom_plans - FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, from_sql, generic_plans, custom_plans); + p.custom_plans, + p.parameter_orig_tables, + p.parameter_orig_columns + FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, from_sql, generic_plans, custom_plans, parameter_orig_tables, parameter_orig_columns); pg_prepared_xacts| SELECT p.transaction, p.gid, p.prepared, diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out index d3ac08c9ee..fc0c5896e4 100644 --- a/src/test/regress/expected/type_sanity.out +++ b/src/test/regress/expected/type_sanity.out @@ -17,7 +17,7 @@ SELECT t1.oid, t1.typname FROM pg_type as t1 WHERE t1.typnamespace = 0 OR (t1.typlen <= 0 AND t1.typlen != -1 AND t1.typlen != -2) OR - (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r')) OR + (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR NOT t1.typisdefined OR (t1.typalign not in ('c', 's', 'i', 'd')) OR (t1.typstorage not in ('p', 'x', 'e', 'm')); @@ -75,7 +75,9 @@ ORDER BY t1.oid; 4600 | pg_brin_bloom_summary 4601 | pg_brin_minmax_multi_summary 5017 | pg_mcv_list -(6 rows) + 8243 | pg_encrypted_det + 8244 | pg_encrypted_rnd +(8 rows) -- Make sure typarray points to a "true" array type of our own base SELECT t1.oid, t1.typname as basetype, t2.typname as arraytype, @@ -210,7 +212,8 @@ ORDER BY 1; e | enum_in m | multirange_in r | range_in -(5 rows) + y | byteain +(6 rows) -- Check for bogus typoutput routines -- As of 8.0, this check finds refcursor, which is borrowing @@ -255,7 +258,8 @@ ORDER BY 1; e | enum_out m | multirange_out r | range_out -(4 rows) + y | byteaout +(5 rows) -- Domains should have same typoutput as their base types SELECT t1.oid, t1.typname, t2.oid, t2.typname @@ -335,7 +339,8 @@ ORDER BY 1; e | enum_recv m | multirange_recv r | range_recv -(5 rows) + y | bytearecv +(6 rows) -- Check for bogus typsend routines -- As of 7.4, this check finds refcursor, which is borrowing @@ -380,7 +385,8 @@ ORDER BY 1; e | enum_send m | multirange_send r | range_send -(4 rows) + y | byteasend +(5 rows) -- Domains should have same typsend as their base types SELECT t1.oid, t1.typname, t2.oid, t2.typname @@ -707,6 +713,8 @@ CREATE TABLE tab_core_types AS SELECT 'txt'::text, true::bool, E'\\xDEADBEEF'::bytea, + E'\\xDEADBEEF'::pg_encrypted_rnd, + E'\\xDEADBEEF'::pg_encrypted_det, B'10001'::bit, B'10001'::varbit AS varbit, '12.34'::money, diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 103e11483d..ffca206c6f 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -129,6 +129,9 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr # ---------- test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats +# WIP +test: column_encryption + # event_trigger cannot run concurrently with any test that runs DDL # oidjoins is read-only, though, and should run late for best coverage test: event_trigger oidjoins diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c index a4b354c9e6..8ad1f458d5 100644 --- a/src/test/regress/pg_regress_main.c +++ b/src/test/regress/pg_regress_main.c @@ -82,7 +82,7 @@ psql_start_test(const char *testname, bindir ? bindir : "", bindir ? "/" : "", dblist->str, - "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on", + "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on", infile, outfile); if (offset >= sizeof(psql_cmd)) diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql new file mode 100644 index 0000000000..37f0b23d10 --- /dev/null +++ b/src/test/regress/sql/column_encryption.sql @@ -0,0 +1,50 @@ +\set HIDE_COLUMN_ENCRYPTION false + +/* Imagine: +CREATE COLUMN MASTER KEY cmk1 ( + realm = 'test' +); +*/ +INSERT INTO pg_colmasterkey (oid, cmkname, cmkowner, cmkrealm) VALUES ( + pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'), + 'cmk1', + (select oid from pg_roles where rolname = current_user), + 'test' +); + +/* Imagine: +CREATE COLUMN ENCRYPTION KEY cek1 ( + column_master_key = cmk1, + algorithm = '...', + encrypted_value = '...' +); +*/ +INSERT INTO pg_colenckey (oid, cekname, cekowner) VALUES ( + pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'), + 'cek1', + (select oid from pg_roles where rolname = current_user) +); +INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES ( + pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'), + (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'), + (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'), + 1, + '\xDEADBEEF' +); + +CREATE TABLE tbl_fail ( + a int, + b text, + c text ENCRYPTED WITH (column_encryption_key = notexist) +); + +CREATE TABLE tbl_29f3 ( + a int, + b text, + c text ENCRYPTED WITH (column_encryption_key = cek1) +); + +\d tbl_29f3 +\d+ tbl_29f3 + +DROP TABLE tbl_29f3; -- FIXME: needs pg_dump support for pg_upgrade tests diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql index 985d0f05c9..b2aa96d370 100644 --- a/src/test/regress/sql/prepare.sql +++ b/src/test/regress/sql/prepare.sql @@ -71,7 +71,11 @@ CREATE TEMPORARY TABLE q5_prep_nodata AS EXECUTE q5(200, 'DTAAAA') PREPARE q7(unknown) AS SELECT * FROM road WHERE thepath = $1; -SELECT name, statement, parameter_types FROM pg_prepared_statements +-- DML statements +PREPARE q8 AS + UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1; + +SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns FROM pg_prepared_statements ORDER BY name; -- test DEALLOCATE ALL; diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql index 1149c6a839..e88c70d302 100644 --- a/src/test/regress/sql/psql.sql +++ b/src/test/regress/sql/psql.sql @@ -119,6 +119,23 @@ CREATE TABLE bububu(a int) \gdesc -- all on one line SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g + +-- \gencr +-- (This just tests the parameter passing; there is no encryption here.) + +CREATE TABLE test_gencr (a int, b text); +INSERT INTO test_gencr VALUES (1, 'one') \gencr +SELECT * FROM test_gencr WHERE a = 1 \gencr + +INSERT INTO test_gencr VALUES ($1, $2) \gencr 2 'two' +SELECT * FROM test_gencr WHERE a IN ($1, $2) \gencr 2 3 + +-- test parse error +SELECT * FROM test_gencr WHERE a = x \gencr +-- test bind error +SELECT * FROM test_gencr WHERE a = $1 \gencr + + -- \gexec create temporary table gexec_test(a int, b text, c date, d float); diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql index 5edc1f1f6e..34dd19456d 100644 --- a/src/test/regress/sql/type_sanity.sql +++ b/src/test/regress/sql/type_sanity.sql @@ -20,7 +20,7 @@ FROM pg_type as t1 WHERE t1.typnamespace = 0 OR (t1.typlen <= 0 AND t1.typlen != -1 AND t1.typlen != -2) OR - (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r')) OR + (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR NOT t1.typisdefined OR (t1.typalign not in ('c', 's', 'i', 'd')) OR (t1.typstorage not in ('p', 'x', 'e', 'm')); @@ -529,6 +529,8 @@ CREATE TABLE tab_core_types AS SELECT 'txt'::text, true::bool, E'\\xDEADBEEF'::bytea, + E'\\xDEADBEEF'::pg_encrypted_rnd, + E'\\xDEADBEEF'::pg_encrypted_det, B'10001'::bit, B'10001'::varbit AS varbit, '12.34'::money, diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h index 971a74cf22..db1f3b8811 100644 --- a/src/include/access/printtup.h +++ b/src/include/access/printtup.h @@ -20,6 +20,8 @@ extern DestReceiver *printtup_create_DR(CommandDest dest); extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal); +extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek); + extern void SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo, List *targetlist, int16 *formats); diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat index 61cd591430..a0bd708132 100644 --- a/src/include/catalog/pg_amop.dat +++ b/src/include/catalog/pg_amop.dat @@ -1028,6 +1028,11 @@ amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)', amopmethod => 'hash' }, +# pg_encrypted_det_ops +{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det', + amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)', + amopmethod => 'hash' }, + # xid_ops { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid', amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' }, diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat index 4cc129bebd..dddb27113f 100644 --- a/src/include/catalog/pg_amproc.dat +++ b/src/include/catalog/pg_amproc.dat @@ -402,6 +402,11 @@ { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea', amprocrighttype => 'bytea', amprocnum => '2', amproc => 'hashvarlenaextended' }, +{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det', + amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' }, +{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det', + amprocrighttype => 'pg_encrypted_det', amprocnum => '2', + amproc => 'hashvarlenaextended' }, { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid', amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' }, { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid', diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h index 053294c99f..550a53649b 100644 --- a/src/include/catalog/pg_attribute.h +++ b/src/include/catalog/pg_attribute.h @@ -164,6 +164,15 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75, */ bool attislocal BKI_DEFAULT(t); + /* column encryption key */ + Oid attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey); + + /* real type if encrypted */ + Oid attrealtypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type); + + /* encryption algorithm (PG_CEK_* values) */ + int16 attencalg BKI_DEFAULT(0); + /* Number of times inherited from direct parent relation(s) */ int32 attinhcount BKI_DEFAULT(0); diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat index 4471eb6bbe..878b0bcbbf 100644 --- a/src/include/catalog/pg_cast.dat +++ b/src/include/catalog/pg_cast.dat @@ -546,4 +546,10 @@ { castsource => 'tstzrange', casttarget => 'tstzmultirange', castfunc => 'tstzmultirange(tstzrange)', castcontext => 'e', castmethod => 'f' }, + +{ castsource => 'bytea', casttarget => 'pg_encrypted_det', castfunc => '0', + castcontext => 'e', castmethod => 'b' }, +{ castsource => 'bytea', casttarget => 'pg_encrypted_rnd', castfunc => '0', + castcontext => 'e', castmethod => 'b' }, + ] diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h new file mode 100644 index 0000000000..095ed712b0 --- /dev/null +++ b/src/include/catalog/pg_colenckey.h @@ -0,0 +1,55 @@ +/*------------------------------------------------------------------------- + * + * pg_colenckey.h + * definition of the "column encryption key" system catalog + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_colenkey.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_COLENCKEY_H +#define PG_COLENCKEY_H + +#include "catalog/genbki.h" +#include "catalog/pg_colenckey_d.h" + +/* ---------------- + * pg_colenckey definition. cpp turns this into + * typedef struct FormData_pg_colenckey + * ---------------- + */ +CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId) +{ + Oid oid; + NameData cekname; + Oid cekowner BKI_LOOKUP(pg_authid); +} FormData_pg_colenckey; + +typedef FormData_pg_colenckey *Form_pg_colenckey; + +DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_index, 8242, ColumnEncKeyNameIndexId, on pg_colenckey using btree(cekname name_ops)); + +/* + * Constants for CMK and CEK algorithms. Note that these are part of the + * protocol. For clarity, the assigned numbers are not reused between CMKs + * and CEKs, but that is not technically required. In either case, don't + * assign zero, so that that can be used as an invalid value. + */ + +#define PG_CMK_RSAES_OAEP_SHA_1 1 +#define PG_CMK_RSAES_OAEP_SHA_256 2 + +#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256 130 +#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384 131 +#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384 132 +#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512 133 + +#endif diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h new file mode 100644 index 0000000000..3e4dea7218 --- /dev/null +++ b/src/include/catalog/pg_colenckeydata.h @@ -0,0 +1,46 @@ +/*------------------------------------------------------------------------- + * + * pg_colenckeydata.h + * definition of the "column encryption key data" system catalog + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_colenkeydata.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_COLENCKEYDATA_H +#define PG_COLENCKEYDATA_H + +#include "catalog/genbki.h" +#include "catalog/pg_colenckeydata_d.h" + +/* ---------------- + * pg_colenckeydata definition. cpp turns this into + * typedef struct FormData_pg_colenckeydata + * ---------------- + */ +CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId) +{ + Oid oid; + Oid ckdcekid BKI_LOOKUP(pg_colenckey); + Oid ckdcmkid BKI_LOOKUP(pg_colmasterkey); + int16 ckdcmkalg; /* PG_CMK_* values */ +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + bytea ckdencval BKI_FORCE_NOT_NULL; +#endif +} FormData_pg_colenckeydata; + +typedef FormData_pg_colenckeydata *Form_pg_colenckeydata; + +DECLARE_TOAST(pg_colenckeydata, 8237, 8238); + +DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops)); + +#endif diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h new file mode 100644 index 0000000000..0344cc4201 --- /dev/null +++ b/src/include/catalog/pg_colmasterkey.h @@ -0,0 +1,45 @@ +/*------------------------------------------------------------------------- + * + * pg_colmasterkey.h + * definition of the "column master key" system catalog + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/catalog/pg_colmasterkey.h + * + * NOTES + * The Catalog.pm module reads this file and derives schema + * information. + * + *------------------------------------------------------------------------- + */ +#ifndef PG_COLMASTERKEY_H +#define PG_COLMASTERKEY_H + +#include "catalog/genbki.h" +#include "catalog/pg_colmasterkey_d.h" + +/* ---------------- + * pg_colmasterkey definition. cpp turns this into + * typedef struct FormData_pg_colmasterkey + * ---------------- + */ +CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId) +{ + Oid oid; + NameData cmkname; + Oid cmkowner BKI_LOOKUP(pg_authid); +#ifdef CATALOG_VARLEN /* variable-length fields start here */ + text cmkrealm BKI_FORCE_NOT_NULL; +#endif +} FormData_pg_colmasterkey; + +typedef FormData_pg_colmasterkey *Form_pg_colmasterkey; + +DECLARE_TOAST(pg_colmasterkey, 8235, 8236); + +DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_index, 8241, ColumnMasterKeyNameIndexId, on pg_colmasterkey using btree(cmkname name_ops)); + +#endif diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat index dbcae7ffdd..0ca401ffe4 100644 --- a/src/include/catalog/pg_opclass.dat +++ b/src/include/catalog/pg_opclass.dat @@ -166,6 +166,8 @@ opcintype => 'bool' }, { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops', opcintype => 'bytea' }, +{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops', + opcintype => 'pg_encrypted_det' }, { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops', opcintype => 'tid' }, { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops', diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat index bc5f8213f3..4737a7f9ed 100644 --- a/src/include/catalog/pg_operator.dat +++ b/src/include/catalog/pg_operator.dat @@ -3458,4 +3458,14 @@ oprcode => 'multirange_after_multirange', oprrest => 'multirangesel', oprjoin => 'scalargtjoinsel' }, +{ oid => '8247', descr => 'equal', + oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det', + oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)', + oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel', + oprjoin => 'eqjoinsel' }, +{ oid => '8248', descr => 'not equal', + oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool', + oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)', + oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' }, + ] diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat index b3b6a7e616..5343580b06 100644 --- a/src/include/catalog/pg_opfamily.dat +++ b/src/include/catalog/pg_opfamily.dat @@ -108,6 +108,8 @@ opfmethod => 'hash', opfname => 'bool_ops' }, { oid => '2223', opfmethod => 'hash', opfname => 'bytea_ops' }, +{ oid => '8249', + opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' }, { oid => '2789', opfmethod => 'btree', opfname => 'tid_ops' }, { oid => '2225', diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 87aa571a33..6b32698c2b 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8025,9 +8025,9 @@ proname => 'pg_prepared_statement', prorows => '1000', proretset => 't', provolatile => 's', proparallel => 'r', prorettype => 'record', proargtypes => '', - proallargtypes => '{text,text,timestamptz,_regtype,bool,int8,int8}', - proargmodes => '{o,o,o,o,o,o,o}', - proargnames => '{name,statement,prepare_time,parameter_types,from_sql,generic_plans,custom_plans}', + proallargtypes => '{text,text,timestamptz,_regtype,bool,int8,int8,_regclass,_int2}', + proargmodes => '{o,o,o,o,o,o,o,o,o}', + proargnames => '{name,statement,prepare_time,parameter_types,from_sql,generic_plans,custom_plans,parameter_orig_tables,parameter_orig_columns}', prosrc => 'pg_prepared_statement' }, { oid => '2511', descr => 'get the open cursors for this session', proname => 'pg_cursor', prorows => '1000', proretset => 't', @@ -11885,4 +11885,11 @@ prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' }, +{ oid => '8245', + proname => 'pg_encrypted_det_eq', proleakproof => 't', prorettype => 'bool', + proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteaeq' }, +{ oid => '8246', + proname => 'pg_encrypted_det_ne', proleakproof => 't', prorettype => 'bool', + proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteane' }, + ] diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat index df45879463..df1fa3e393 100644 --- a/src/include/catalog/pg_type.dat +++ b/src/include/catalog/pg_type.dat @@ -692,4 +692,16 @@ typreceive => 'brin_minmax_multi_summary_recv', typsend => 'brin_minmax_multi_summary_send', typalign => 'i', typstorage => 'x', typcollation => 'default' }, + +{ oid => '8243', descr => 'encrypted column (deterministic)', + typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'y', + typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout', + typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i', + typstorage => 'e' }, +{ oid => '8244', descr => 'encrypted column (randomized)', + typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'y', + typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout', + typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i', + typstorage => 'e' }, + ] diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h index 48a2559137..fdb096b1fb 100644 --- a/src/include/catalog/pg_type.h +++ b/src/include/catalog/pg_type.h @@ -277,6 +277,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_ #define TYPTYPE_MULTIRANGE 'm' /* multirange type */ #define TYPTYPE_PSEUDO 'p' /* pseudo-type */ #define TYPTYPE_RANGE 'r' /* range type */ +#define TYPTYPE_ENCRYPTED 'y' /* encrypted column value */ #define TYPCATEGORY_INVALID '\0' /* not an allowed category */ #define TYPCATEGORY_ARRAY 'A' diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 73f635b455..883ed88466 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -678,6 +678,7 @@ typedef struct ColumnDef char *colname; /* name of column */ TypeName *typeName; /* type of column */ char *compression; /* compression method for column */ + List *encryption; /* encryption info for column */ int inhcount; /* number of times column is inherited */ bool is_local; /* column has local (non-inherited) def'n */ bool is_not_null; /* NOT NULL constraint specified? */ diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h index dc379547c7..c310e124f1 100644 --- a/src/include/parser/analyze.h +++ b/src/include/parser/analyze.h @@ -28,7 +28,9 @@ extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook; extern Query *parse_analyze_fixedparams(RawStmt *parseTree, const char *sourceText, const Oid *paramTypes, int numParams, QueryEnvironment *queryEnv); extern Query *parse_analyze_varparams(RawStmt *parseTree, const char *sourceText, - Oid **paramTypes, int *numParams, QueryEnvironment *queryEnv); + Oid **paramTypes, int *numParams, + Oid **paramOrigTbls, AttrNumber **paramOrigCols, + QueryEnvironment *queryEnv); extern Query *parse_analyze_withcb(RawStmt *parseTree, const char *sourceText, ParserSetupHook parserSetup, void *parserSetupArg, diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index cf9c759025..225a2a8733 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -93,6 +93,7 @@ typedef Node *(*ParseParamRefHook) (ParseState *pstate, ParamRef *pref); typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param, Oid targetTypeId, int32 targetTypeMod, int location); +typedef void (*ParamAssignOrigHook) (ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol); /* @@ -222,6 +223,7 @@ struct ParseState PostParseColumnRefHook p_post_columnref_hook; ParseParamRefHook p_paramref_hook; CoerceParamHook p_coerce_param_hook; + ParamAssignOrigHook p_param_assign_orig_hook; void *p_ref_hook_state; /* common passthrough link for above */ }; diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h index df1ee660d8..da202b28a7 100644 --- a/src/include/parser/parse_param.h +++ b/src/include/parser/parse_param.h @@ -18,7 +18,8 @@ extern void setup_parse_fixed_parameters(ParseState *pstate, const Oid *paramTypes, int numParams); extern void setup_parse_variable_parameters(ParseState *pstate, - Oid **paramTypes, int *numParams); + Oid **paramTypes, int *numParams, + Oid **paramOrigTbls, AttrNumber **paramOrigCols); extern void check_variable_parameters(ParseState *pstate, Query *query); extern bool query_contains_extern_params(Query *query); diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h index 70d9dab25b..3038ceb522 100644 --- a/src/include/tcop/tcopprot.h +++ b/src/include/tcop/tcopprot.h @@ -53,6 +53,8 @@ extern List *pg_analyze_and_rewrite_varparams(RawStmt *parsetree, const char *query_string, Oid **paramTypes, int *numParams, + Oid **paramOrigTbls, + AttrNumber **paramOrigCols, QueryEnvironment *queryEnv); extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree, const char *query_string, diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h index 0499635f59..8f9dcc024b 100644 --- a/src/include/utils/plancache.h +++ b/src/include/utils/plancache.h @@ -101,6 +101,8 @@ typedef struct CachedPlanSource CommandTag commandTag; /* 'nuff said */ Oid *param_types; /* array of parameter type OIDs, or NULL */ int num_params; /* length of param_types array */ + Oid *param_origtbls; /* array of underlying tables of parameters, or NULL */ + AttrNumber *param_origcols; /* array of underlying columns of parameters, or NULL */ ParserSetupHook parserSetup; /* alternative parameter spec method */ void *parserSetupArg; int cursor_options; /* cursor options used for planning */ @@ -199,6 +201,8 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource, MemoryContext querytree_context, Oid *param_types, int num_params, + Oid *param_origtbls, + AttrNumber *param_origcols, ParserSetupHook parserSetup, void *parserSetupArg, int cursor_options, diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h index 4463ea66be..09e2df7d78 100644 --- a/src/include/utils/syscache.h +++ b/src/include/utils/syscache.h @@ -44,8 +44,11 @@ enum SysCacheIdentifier AUTHNAME, AUTHOID, CASTSOURCETARGET, + CEKNAME, + CEKOID, CLAAMNAMENSP, CLAOID, + CMKOID, COLLNAMEENCNSP, COLLOID, CONDEFAULT, diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c index e99aa279f6..79154f8aab 100644 --- a/src/backend/access/common/printsimple.c +++ b/src/backend/access/common/printsimple.c @@ -46,6 +46,8 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc) pq_sendint16(&buf, attr->attlen); pq_sendint32(&buf, attr->atttypmod); pq_sendint16(&buf, 0); /* format code */ + pq_sendint32(&buf, 0); /* CEK */ + pq_sendint16(&buf, 0); /* CEK alg */ } pq_endmessage(&buf); diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c index d2f3b57288..e1d3efab44 100644 --- a/src/backend/access/common/printtup.c +++ b/src/backend/access/common/printtup.c @@ -15,13 +15,25 @@ */ #include "postgres.h" +#include "access/genam.h" #include "access/printtup.h" +#include "access/skey.h" +#include "access/table.h" +#include "catalog/pg_colenckey.h" +#include "catalog/pg_colenckeydata.h" +#include "catalog/pg_colmasterkey.h" #include "libpq/libpq.h" #include "libpq/pqformat.h" #include "tcop/pquery.h" +#include "utils/array.h" +#include "utils/arrayaccess.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/memdebug.h" #include "utils/memutils.h" +#include "utils/rel.h" +#include "utils/syscache.h" static void printtup_startup(DestReceiver *self, int operation, @@ -151,6 +163,111 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo) */ } +/* + * Send ColumnMasterKey message, unless it's already been sent in this session + * for this key. + * + * TODO: syscache invalidation support + */ +List *cmk_sent = NIL; + +static void +MaybeSendColumnMasterKeyMessage(Oid cmkid) +{ + HeapTuple tuple; + Form_pg_colmasterkey cmkform; + Datum datum; + bool isnull; + StringInfoData buf; + MemoryContext oldcontext; + + if (list_member_oid(cmk_sent, cmkid)) + return; + + tuple = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for column master key %u", cmkid); + cmkform = (Form_pg_colmasterkey) GETSTRUCT(tuple); + + pq_beginmessage(&buf, 'y'); /* ColumnMasterKey */ + pq_sendint32(&buf, cmkform->oid); + pq_sendstring(&buf, NameStr(cmkform->cmkname)); + datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkrealm, &isnull); + Assert(!isnull); + pq_sendstring(&buf, TextDatumGetCString(datum)); + pq_endmessage(&buf); + + ReleaseSysCache(tuple); + + oldcontext = MemoryContextSwitchTo(TopMemoryContext); + cmk_sent = lappend_oid(cmk_sent, cmkid); + MemoryContextSwitchTo(oldcontext); +} + +/* + * Send ColumnEncryptionKey message, unless it's already been sent in this + * session for this key. + * + * TODO: syscache invalidation support + */ +List *cek_sent = NIL; + +void +MaybeSendColumnEncryptionKeyMessage(Oid attcek) +{ + HeapTuple tuple; + ScanKeyData skey[1]; + SysScanDesc sd; + Relation rel; + bool found = false; + MemoryContext oldcontext; + + if (list_member_oid(cek_sent, attcek)) + return; + + ScanKeyInit(&skey[0], + Anum_pg_colenckeydata_ckdcekid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(attcek)); + rel = table_open(ColumnEncKeyDataRelationId, AccessShareLock); + sd = systable_beginscan(rel, ColumnEncKeyCekidCmkidIndexId, true, NULL, 1, skey); + + while ((tuple = systable_getnext(sd))) + { + Form_pg_colenckeydata ckdform = (Form_pg_colenckeydata) GETSTRUCT(tuple); + Datum datum; + bool isnull; + bytea *ba; + StringInfoData buf; + + MaybeSendColumnMasterKeyMessage(ckdform->ckdcmkid); + + datum = heap_getattr(tuple, Anum_pg_colenckeydata_ckdencval, RelationGetDescr(rel), &isnull); + Assert(!isnull); + ba = pg_detoast_datum_packed((bytea *) DatumGetPointer(datum)); + + pq_beginmessage(&buf, 'Y'); /* ColumnEncryptionKey */ + pq_sendint32(&buf, ckdform->ckdcekid); + pq_sendint32(&buf, ckdform->ckdcmkid); + pq_sendint16(&buf, ckdform->ckdcmkalg); + pq_sendint32(&buf, VARSIZE_ANY_EXHDR(ba)); + pq_sendbytes(&buf, VARDATA_ANY(ba), VARSIZE_ANY_EXHDR(ba)); + pq_endmessage(&buf); + + found = true; + } + + if (!found) + elog(ERROR, "lookup failed for column encryption key data %u", attcek); + + systable_endscan(sd); + table_close(rel, NoLock); + + oldcontext = MemoryContextSwitchTo(TopMemoryContext); + cek_sent = lappend_oid(cek_sent, attcek); + MemoryContextSwitchTo(oldcontext); +} + /* * SendRowDescriptionMessage --- send a RowDescription message to the frontend * @@ -200,6 +317,8 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo, Oid resorigtbl; AttrNumber resorigcol; int16 format; + Oid attcekid = InvalidOid; + int16 attencalg = 0; /* * If column is a domain, send the base type and typmod instead. @@ -231,6 +350,22 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo, else format = 0; + if (get_typtype(atttypid) == TYPTYPE_ENCRYPTED) + { + HeapTuple tp; + Form_pg_attribute orig_att; + + tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(resorigtbl), Int16GetDatum(resorigcol)); + if (!HeapTupleIsValid(tp)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", resorigcol, resorigtbl); + orig_att = (Form_pg_attribute) GETSTRUCT(tp); + MaybeSendColumnEncryptionKeyMessage(orig_att->attcek); + atttypid = orig_att->attrealtypid; + attcekid = orig_att->attcek; + attencalg = orig_att->attencalg; + ReleaseSysCache(tp); + } + pq_writestring(buf, NameStr(att->attname)); pq_writeint32(buf, resorigtbl); pq_writeint16(buf, resorigcol); @@ -238,6 +373,8 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo, pq_writeint16(buf, att->attlen); pq_writeint32(buf, atttypmod); pq_writeint16(buf, format); + pq_writeint32(buf, attcekid); + pq_writeint16(buf, attencalg); } pq_endmessage_reuse(buf); diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c index 9f41b1e854..38d60b174f 100644 --- a/src/backend/access/common/tupdesc.c +++ b/src/backend/access/common/tupdesc.c @@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2) return false; if (attr1->attislocal != attr2->attislocal) return false; + if (attr1->attcek != attr2->attcek) + return false; + if (attr1->attrealtypid != attr2->attrealtypid) + return false; + if (attr1->attencalg != attr2->attencalg) + return false; if (attr1->attinhcount != attr2->attinhcount) return false; if (attr1->attcollation != attr2->attcollation) @@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc, att->attgenerated = '\0'; att->attisdropped = false; att->attislocal = true; + att->attcek = 0; + att->attrealtypid = 0; + att->attencalg = 0; att->attinhcount = 0; /* variable-length fields are not present in tupledescs */ @@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc, att->attgenerated = '\0'; att->attisdropped = false; att->attislocal = true; + att->attcek = 0; + att->attrealtypid = 0; + att->attencalg = 0; att->attinhcount = 0; /* variable-length fields are not present in tupledescs */ diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c index 10bf26ce7c..cea91d4a88 100644 --- a/src/backend/access/hash/hashvalidate.c +++ b/src/backend/access/hash/hashvalidate.c @@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype) argtype == BOOLOID) /* okay, allowed use of hashchar() */ ; else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) && - argtype == BYTEAOID) + (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID)) /* okay, allowed use of hashvarlena() */ ; else result = false; diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index 89a0221ec9..7e1a91b974 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -72,7 +72,8 @@ CATALOG_HEADERS := \ pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \ pg_range.h pg_transform.h \ pg_sequence.h pg_publication.h pg_publication_namespace.h \ - pg_publication_rel.h pg_subscription.h pg_subscription_rel.h + pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \ + pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 1803194db9..0ea58239b4 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -746,6 +746,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel, slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated); slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped); slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal); + slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek); + slot[slotCount]->tts_values[Anum_pg_attribute_attrealtypid - 1] = ObjectIdGetDatum(attrs->attrealtypid); + slot[slotCount]->tts_values[Anum_pg_attribute_attencalg - 1] = Int16GetDatum(attrs->attencalg); slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount); slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation); if (attoptions && attoptions[natts] != (Datum) 0) diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c index 80738547ed..a17c2731be 100644 --- a/src/backend/commands/prepare.c +++ b/src/backend/commands/prepare.c @@ -50,7 +50,6 @@ static void InitQueryHashTable(void); static ParamListInfo EvaluateParams(ParseState *pstate, PreparedStatement *pstmt, List *params, EState *estate); -static Datum build_regtype_array(Oid *param_types, int num_params); /* * Implements the 'PREPARE' utility statement. @@ -62,6 +61,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt, RawStmt *rawstmt; CachedPlanSource *plansource; Oid *argtypes = NULL; + Oid *argorigtbls = NULL; + AttrNumber *argorigcols = NULL; int nargs; List *query_list; @@ -108,6 +109,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt, argtypes[i++] = toid; } + + argorigtbls = (Oid *) palloc0(nargs * sizeof(Oid)); + argorigcols = (AttrNumber *) palloc0(nargs * sizeof(AttrNumber)); } /* @@ -117,7 +121,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt, * Rewrite the query. The result could be 0, 1, or many queries. */ query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext, - &argtypes, &nargs, NULL); + &argtypes, &nargs, + &argorigtbls, &argorigcols, + NULL); /* Finish filling in the CachedPlanSource */ CompleteCachedPlan(plansource, @@ -125,6 +131,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt, NULL, argtypes, nargs, + argorigtbls, + argorigcols, NULL, NULL, CURSOR_OPT_PARALLEL_OK, /* allow parallel mode */ @@ -683,20 +691,36 @@ pg_prepared_statement(PG_FUNCTION_ARGS) hash_seq_init(&hash_seq, prepared_queries); while ((prep_stmt = hash_seq_search(&hash_seq)) != NULL) { - Datum values[7]; - bool nulls[7]; + Datum values[9]; + bool nulls[9]; + int num_params = prep_stmt->plansource->num_params; + Datum *tmp_ary; MemSet(nulls, 0, sizeof(nulls)); values[0] = CStringGetTextDatum(prep_stmt->stmt_name); values[1] = CStringGetTextDatum(prep_stmt->plansource->query_string); values[2] = TimestampTzGetDatum(prep_stmt->prepare_time); - values[3] = build_regtype_array(prep_stmt->plansource->param_types, - prep_stmt->plansource->num_params); + + tmp_ary = (Datum *) palloc(num_params * sizeof(Datum)); + for (int i = 0; i < num_params; i++) + tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_types[i]); + values[3] = PointerGetDatum(construct_array(tmp_ary, num_params, REGTYPEOID, 4, true, TYPALIGN_INT)); + values[4] = BoolGetDatum(prep_stmt->from_sql); values[5] = Int64GetDatumFast(prep_stmt->plansource->num_generic_plans); values[6] = Int64GetDatumFast(prep_stmt->plansource->num_custom_plans); + tmp_ary = (Datum *) palloc(num_params * sizeof(Datum)); + for (int i = 0; i < num_params; i++) + tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_origtbls[i]); + values[7] = PointerGetDatum(construct_array(tmp_ary, num_params, REGCLASSOID, 4, true, TYPALIGN_INT)); + + tmp_ary = (Datum *) palloc(num_params * sizeof(Datum)); + for (int i = 0; i < num_params; i++) + tmp_ary[i] = Int16GetDatum(prep_stmt->plansource->param_origcols[i]); + values[8] = PointerGetDatum(construct_array(tmp_ary, num_params, INT2OID, 2, true, TYPALIGN_SHORT)); + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); } @@ -704,26 +728,3 @@ pg_prepared_statement(PG_FUNCTION_ARGS) return (Datum) 0; } - -/* - * This utility function takes a C array of Oids, and returns a Datum - * pointing to a one-dimensional Postgres array of regtypes. An empty - * array is returned as a zero-element array, not NULL. - */ -static Datum -build_regtype_array(Oid *param_types, int num_params) -{ - Datum *tmp_ary; - ArrayType *result; - int i; - - tmp_ary = (Datum *) palloc(num_params * sizeof(Datum)); - - for (i = 0; i < num_params; i++) - tmp_ary[i] = ObjectIdGetDatum(param_types[i]); - - /* XXX: this hardcodes assumptions about the regtype type */ - result = construct_array(tmp_ary, num_params, REGTYPEOID, - 4, true, TYPALIGN_INT); - return PointerGetDatum(result); -} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2de0ebacec..a0a9c6b59a 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -35,6 +35,7 @@ #include "catalog/partition.h" #include "catalog/pg_am.h" #include "catalog/pg_attrdef.h" +#include "catalog/pg_colenckey.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" #include "catalog/pg_depend.h" @@ -931,6 +932,76 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, if (colDef->compression) attr->attcompression = GetAttributeCompression(attr->atttypid, colDef->compression); + + if (colDef->encryption) + { + ListCell *lc; + char *cek = NULL; + Oid cekoid; + bool encdet = false; + int alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256; + + foreach(lc, colDef->encryption) + { + DefElem *el = lfirst_node(DefElem, lc); + + if (strcmp(el->defname, "column_encryption_key") == 0) + cek = strVal(linitial(castNode(TypeName, el->arg)->names)); + else if (strcmp(el->defname, "encryption_type") == 0) + { + char *val = strVal(linitial(castNode(TypeName, el->arg)->names)); + + if (strcmp(val, "deterministic") == 0) + encdet = true; + else if (strcmp(val, "randomized") == 0) + encdet = false; + else + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized encryption type: %s", val)); + } + else if (strcmp(el->defname, "algorithm") == 0) + { + char *val = strVal(el->arg); + + if (strcmp(val, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0) + alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256; + else if (strcmp(val, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0) + alg = PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384; + else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0) + alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384; + else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0) + alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512; + else + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized encryption algorithm: %s", val)); + } + else + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized column encryption parameter: %s", el->defname)); + } + + if (!cek) + ereport(ERROR, + errcode(ERRCODE_INVALID_COLUMN_DEFINITION), + errmsg("column encryption key must be specified")); + + cekoid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid, PointerGetDatum(cek)); + if (!cekoid) + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("column encryption key \"%s\" does not exist", cek)); + + attr->attcek = cekoid; + attr->attrealtypid = attr->atttypid; + if (encdet) + attr->atttypid = PG_ENCRYPTED_DETOID; + else + attr->atttypid = PG_ENCRYPTED_RNDOID; + attr->attencalg = alg; + } } /* @@ -6829,6 +6900,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, attribute.attgenerated = colDef->generated; attribute.attisdropped = false; attribute.attislocal = colDef->is_local; + attribute.attcek = 0; // TODO + attribute.attrealtypid = 0; // TODO + attribute.attencalg = 0; // TODO attribute.attinhcount = colDef->inhcount; attribute.attcollation = collOid; diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index 29bc26669b..cde5b0e087 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -2279,6 +2279,8 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan) NULL, plan->argtypes, plan->nargs, + NULL, + NULL, plan->parserSetup, plan->parserSetupArg, plan->cursor_options, @@ -2516,6 +2518,8 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options, NULL, plan->argtypes, plan->nargs, + NULL, + NULL, plan->parserSetup, plan->parserSetupArg, plan->cursor_options, diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 51d630fa89..c901cfbb86 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -3555,6 +3555,7 @@ _copyColumnDef(const ColumnDef *from) COPY_STRING_FIELD(colname); COPY_NODE_FIELD(typeName); COPY_STRING_FIELD(compression); + COPY_NODE_FIELD(encryption); COPY_SCALAR_FIELD(inhcount); COPY_SCALAR_FIELD(is_local); COPY_SCALAR_FIELD(is_not_null); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index e747e1667d..69a3adee7c 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -3045,6 +3045,7 @@ _equalColumnDef(const ColumnDef *a, const ColumnDef *b) COMPARE_STRING_FIELD(colname); COMPARE_NODE_FIELD(typeName); COMPARE_STRING_FIELD(compression); + COMPARE_NODE_FIELD(encryption); COMPARE_SCALAR_FIELD(inhcount); COMPARE_SCALAR_FIELD(is_local); COMPARE_SCALAR_FIELD(is_not_null); diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 4cb1744da6..8fbe0f5f8f 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -4288,6 +4288,8 @@ raw_expression_tree_walker(Node *node, return true; if (walker(coldef->compression, context)) return true; + if (walker(coldef->encryption, context)) + return true; if (walker(coldef->raw_default, context)) return true; if (walker(coldef->collClause, context)) diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index ce12915592..2bfc259e4e 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -3112,6 +3112,7 @@ _outColumnDef(StringInfo str, const ColumnDef *node) WRITE_STRING_FIELD(colname); WRITE_NODE_FIELD(typeName); WRITE_STRING_FIELD(compression); + WRITE_NODE_FIELD(encryption); WRITE_INT_FIELD(inhcount); WRITE_BOOL_FIELD(is_local); WRITE_BOOL_FIELD(is_not_null); diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c index 1bcb875507..edc9fb4a28 100644 --- a/src/backend/parser/analyze.c +++ b/src/backend/parser/analyze.c @@ -145,6 +145,7 @@ parse_analyze_fixedparams(RawStmt *parseTree, const char *sourceText, Query * parse_analyze_varparams(RawStmt *parseTree, const char *sourceText, Oid **paramTypes, int *numParams, + Oid **paramOrigTbls, AttrNumber **paramOrigCols, QueryEnvironment *queryEnv) { ParseState *pstate = make_parsestate(NULL); @@ -155,7 +156,7 @@ parse_analyze_varparams(RawStmt *parseTree, const char *sourceText, pstate->p_sourcetext = sourceText; - setup_parse_variable_parameters(pstate, paramTypes, numParams); + setup_parse_variable_parameters(pstate, paramTypes, numParams, paramOrigTbls, paramOrigCols); pstate->p_queryEnv = queryEnv; diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 969c9c158f..a918eaf427 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -596,6 +596,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type TableConstraint TableLikeClause %type TableLikeOptionList TableLikeOption %type column_compression opt_column_compression +%type opt_column_encryption %type ColQualList %type ColConstraint ColConstraintElem ConstraintAttr %type key_match @@ -3778,13 +3779,14 @@ TypedTableElement: | TableConstraint { $$ = $1; } ; -columnDef: ColId Typename opt_column_compression create_generic_options ColQualList +columnDef: ColId Typename opt_column_compression opt_column_encryption create_generic_options ColQualList { ColumnDef *n = makeNode(ColumnDef); n->colname = $1; n->typeName = $2; n->compression = $3; + n->encryption = $4; n->inhcount = 0; n->is_local = true; n->is_not_null = false; @@ -3793,8 +3795,8 @@ columnDef: ColId Typename opt_column_compression create_generic_options ColQualL n->raw_default = NULL; n->cooked_default = NULL; n->collOid = InvalidOid; - n->fdwoptions = $4; - SplitColQualList($5, &n->constraints, &n->collClause, + n->fdwoptions = $5; + SplitColQualList($6, &n->constraints, &n->collClause, yyscanner); n->location = @1; $$ = (Node *) n; @@ -3851,6 +3853,11 @@ opt_column_compression: | /*EMPTY*/ { $$ = NULL; } ; +opt_column_encryption: + ENCRYPTED WITH '(' def_list ')' { $$ = $4; } + | /*EMPTY*/ { $$ = NULL; } + ; + ColQualList: ColQualList ColConstraint { $$ = lappend($1, $2); } | /*EMPTY*/ { $$ = NIL; } diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c index f668abfcb3..c6287444a8 100644 --- a/src/backend/parser/parse_param.c +++ b/src/backend/parser/parse_param.c @@ -49,6 +49,8 @@ typedef struct VarParamState { Oid **paramTypes; /* array of parameter type OIDs */ int *numParams; /* number of array entries */ + Oid **paramOrigTbls; /* underlying tables (0 if none) */ + AttrNumber **paramOrigCols; /* underlying columns (0 if none) */ } VarParamState; static Node *fixed_paramref_hook(ParseState *pstate, ParamRef *pref); @@ -56,6 +58,7 @@ static Node *variable_paramref_hook(ParseState *pstate, ParamRef *pref); static Node *variable_coerce_param_hook(ParseState *pstate, Param *param, Oid targetTypeId, int32 targetTypeMod, int location); +static void variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol); static bool check_parameter_resolution_walker(Node *node, ParseState *pstate); static bool query_contains_extern_params_walker(Node *node, void *context); @@ -81,15 +84,19 @@ setup_parse_fixed_parameters(ParseState *pstate, */ void setup_parse_variable_parameters(ParseState *pstate, - Oid **paramTypes, int *numParams) + Oid **paramTypes, int *numParams, + Oid **paramOrigTbls, AttrNumber **paramOrigCols) { VarParamState *parstate = palloc(sizeof(VarParamState)); parstate->paramTypes = paramTypes; parstate->numParams = numParams; + parstate->paramOrigTbls = paramOrigTbls; + parstate->paramOrigCols = paramOrigCols; pstate->p_ref_hook_state = (void *) parstate; pstate->p_paramref_hook = variable_paramref_hook; pstate->p_coerce_param_hook = variable_coerce_param_hook; + pstate->p_param_assign_orig_hook = variable_param_assign_orig_hook; } /* @@ -145,14 +152,36 @@ variable_paramref_hook(ParseState *pstate, ParamRef *pref) { /* Need to enlarge param array */ if (*parstate->paramTypes) + { *parstate->paramTypes = (Oid *) repalloc(*parstate->paramTypes, paramno * sizeof(Oid)); + if (parstate->paramOrigTbls) + *parstate->paramOrigTbls = repalloc(*parstate->paramOrigTbls, + paramno * sizeof(Oid)); + if (parstate->paramOrigCols) + *parstate->paramOrigCols = repalloc(*parstate->paramOrigCols, + paramno * sizeof(AttrNumber)); + } else + { *parstate->paramTypes = (Oid *) palloc(paramno * sizeof(Oid)); + if (parstate->paramOrigTbls) + *parstate->paramOrigTbls = palloc(paramno * sizeof(Oid)); + if (parstate->paramOrigCols) + *parstate->paramOrigCols = palloc(paramno * sizeof(AttrNumber)); + } /* Zero out the previously-unreferenced slots */ MemSet(*parstate->paramTypes + *parstate->numParams, 0, (paramno - *parstate->numParams) * sizeof(Oid)); + if (parstate->paramOrigTbls) + MemSet(*parstate->paramOrigTbls + *parstate->numParams, + 0, + (paramno - *parstate->numParams) * sizeof(Oid)); + if (parstate->paramOrigCols) + MemSet(*parstate->paramOrigCols + *parstate->numParams, + 0, + (paramno - *parstate->numParams) * sizeof(AttrNumber)); *parstate->numParams = paramno; } @@ -260,6 +289,18 @@ variable_coerce_param_hook(ParseState *pstate, Param *param, return NULL; } +static void +variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol) +{ + VarParamState *parstate = (VarParamState *) pstate->p_ref_hook_state; + int paramno = param->paramid; + + if (parstate->paramOrigTbls) + (*parstate->paramOrigTbls)[paramno - 1] = origtbl; + if (parstate->paramOrigCols) + (*parstate->paramOrigCols)[paramno - 1] = origcol; +} + /* * Check for consistent assignment of variable parameters after completion * of parsing with parse_variable_parameters. diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 2a1d44b813..b964088044 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -595,6 +595,12 @@ transformAssignedExpr(ParseState *pstate, parser_errposition(pstate, exprLocation(orig_expr)))); } + if (IsA(expr, Param)) + { + if (pstate->p_param_assign_orig_hook) + pstate->p_param_assign_orig_hook(pstate, castNode(Param, expr), RelationGetRelid(pstate->p_target_relation), attrno); + } + pstate->p_expr_kind = sv_expr_kind; return expr; diff --git a/src/backend/replication/basebackup_copy.c b/src/backend/replication/basebackup_copy.c index cabb077240..24f4a1091a 100644 --- a/src/backend/replication/basebackup_copy.c +++ b/src/backend/replication/basebackup_copy.c @@ -351,6 +351,8 @@ SendXlogRecPtrResult(XLogRecPtr ptr, TimeLineID tli) pq_sendint16(&buf, -1); pq_sendint32(&buf, 0); pq_sendint16(&buf, 0); + pq_sendint32(&buf, 0); + pq_sendint16(&buf, 0); pq_sendstring(&buf, "tli"); pq_sendint32(&buf, 0); /* table oid */ @@ -364,6 +366,9 @@ SendXlogRecPtrResult(XLogRecPtr ptr, TimeLineID tli) pq_sendint16(&buf, -1); pq_sendint32(&buf, 0); pq_sendint16(&buf, 0); + pq_sendint32(&buf, 0); + pq_sendint16(&buf, 0); + pq_endmessage(&buf); /* Data row */ @@ -406,6 +411,8 @@ SendTablespaceList(List *tablespaces) pq_sendint16(&buf, 4); /* typlen */ pq_sendint32(&buf, 0); /* typmod */ pq_sendint16(&buf, 0); /* format code */ + pq_sendint32(&buf, 0); + pq_sendint16(&buf, 0); /* Second field - spclocation */ pq_sendstring(&buf, "spclocation"); @@ -415,6 +422,8 @@ SendTablespaceList(List *tablespaces) pq_sendint16(&buf, -1); pq_sendint32(&buf, 0); pq_sendint16(&buf, 0); + pq_sendint32(&buf, 0); + pq_sendint16(&buf, 0); /* Third field - size */ pq_sendstring(&buf, "size"); @@ -424,6 +433,9 @@ SendTablespaceList(List *tablespaces) pq_sendint16(&buf, 8); pq_sendint32(&buf, 0); pq_sendint16(&buf, 0); + pq_sendint32(&buf, 0); + pq_sendint16(&buf, 0); + pq_endmessage(&buf); foreach(lc, tablespaces) diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index e42671722a..7200b55318 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -607,6 +607,8 @@ SendTimeLineHistory(TimeLineHistoryCmd *cmd) pq_sendint16(&buf, -1); /* typlen */ pq_sendint32(&buf, 0); /* typmod */ pq_sendint16(&buf, 0); /* format code */ + pq_sendint32(&buf, 0); + pq_sendint16(&buf, 0); /* second field */ pq_sendstring(&buf, "content"); /* col name */ @@ -616,6 +618,9 @@ SendTimeLineHistory(TimeLineHistoryCmd *cmd) pq_sendint16(&buf, -1); /* typlen */ pq_sendint32(&buf, 0); /* typmod */ pq_sendint16(&buf, 0); /* format code */ + pq_sendint32(&buf, 0); + pq_sendint16(&buf, 0); + pq_endmessage(&buf); /* Send a DataRow message */ diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 8b6b5bbaaa..a574194701 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -79,6 +79,7 @@ #include "utils/memutils.h" #include "utils/ps_status.h" #include "utils/snapmgr.h" +#include "utils/syscache.h" #include "utils/timeout.h" #include "utils/timestamp.h" @@ -680,6 +681,8 @@ pg_analyze_and_rewrite_varparams(RawStmt *parsetree, const char *query_string, Oid **paramTypes, int *numParams, + Oid **paramOrigTbls, + AttrNumber **paramOrigCols, QueryEnvironment *queryEnv) { Query *query; @@ -694,7 +697,7 @@ pg_analyze_and_rewrite_varparams(RawStmt *parsetree, ResetUsage(); query = parse_analyze_varparams(parsetree, query_string, paramTypes, numParams, - queryEnv); + paramOrigTbls, paramOrigCols, queryEnv); /* * Check all parameter types got determined. @@ -1371,6 +1374,8 @@ exec_parse_message(const char *query_string, /* string to execute */ bool is_named; bool save_log_statement_stats = log_statement_stats; char msec_str[32]; + Oid *paramOrigTbls = palloc(numParams * sizeof(Oid)); + AttrNumber *paramOrigCols = palloc(numParams * sizeof(AttrNumber)); /* * Report query to various monitoring facilities. @@ -1491,6 +1496,8 @@ exec_parse_message(const char *query_string, /* string to execute */ query_string, ¶mTypes, &numParams, + ¶mOrigTbls, + ¶mOrigCols, NULL); /* Done with the snapshot used for parsing */ @@ -1521,6 +1528,8 @@ exec_parse_message(const char *query_string, /* string to execute */ unnamed_stmt_context, paramTypes, numParams, + paramOrigTbls, + paramOrigCols, NULL, NULL, CURSOR_OPT_PARALLEL_OK, /* allow parallel mode */ @@ -1818,6 +1827,16 @@ exec_bind_message(StringInfo input_message) else pformat = 0; /* default = text */ + if (get_typtype(ptype) == TYPTYPE_ENCRYPTED) + { + if (pformat & 0xF0) + pformat &= ~0xF0; + else + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("parameter corresponds to an encrypted column, but the parameter value was not encrypted"))); + } + if (pformat == 0) /* text mode */ { Oid typinput; @@ -2614,8 +2633,41 @@ exec_describe_statement_message(const char *stmt_name) for (int i = 0; i < psrc->num_params; i++) { Oid ptype = psrc->param_types[i]; + Oid pcekid = InvalidOid; + int pcekalg = 0; + int16 pflags = 0; + + if (get_typtype(ptype) == TYPTYPE_ENCRYPTED) + { + Oid porigtbl = psrc->param_origtbls[i]; + AttrNumber porigcol = psrc->param_origcols[i]; + HeapTuple tp; + Form_pg_attribute orig_att; + + if (porigtbl == InvalidOid || porigcol <= 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("parameter %d corresponds to an encrypted column, but an underlying table and column could not be determined", i))); + + tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(porigtbl), Int16GetDatum(porigcol)); + if (!HeapTupleIsValid(tp)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", porigcol, porigtbl); + orig_att = (Form_pg_attribute) GETSTRUCT(tp); + ptype = orig_att->attrealtypid; + pcekid = orig_att->attcek; + pcekalg = orig_att->attencalg; + ReleaseSysCache(tp); + + if (psrc->param_types[i] == PG_ENCRYPTED_DETOID) + pflags |= 0x01; + + MaybeSendColumnEncryptionKeyMessage(pcekid); + } pq_sendint32(&row_description_buf, (int) ptype); + pq_sendint32(&row_description_buf, (int) pcekid); + pq_sendint16(&row_description_buf, pcekalg); + pq_sendint16(&row_description_buf, pflags); } pq_endmessage_reuse(&row_description_buf); diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c index 0d6a295674..f6893dd8b7 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -340,6 +340,8 @@ CompleteCachedPlan(CachedPlanSource *plansource, MemoryContext querytree_context, Oid *param_types, int num_params, + Oid *param_origtbls, + AttrNumber *param_origcols, ParserSetupHook parserSetup, void *parserSetupArg, int cursor_options, @@ -419,6 +421,14 @@ CompleteCachedPlan(CachedPlanSource *plansource, { plansource->param_types = (Oid *) palloc(num_params * sizeof(Oid)); memcpy(plansource->param_types, param_types, num_params * sizeof(Oid)); + + plansource->param_origtbls = (Oid *) palloc0(num_params * sizeof(Oid)); + if (param_origtbls) + memcpy(plansource->param_origtbls, param_origtbls, num_params * sizeof(Oid)); + + plansource->param_origcols = (AttrNumber *) palloc0(num_params * sizeof(AttrNumber)); + if (param_origcols) + memcpy(plansource->param_origcols, param_origcols, num_params * sizeof(AttrNumber)); } else plansource->param_types = NULL; diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c index 1912b12146..8fdfaf839d 100644 --- a/src/backend/utils/cache/syscache.c +++ b/src/backend/utils/cache/syscache.c @@ -29,7 +29,9 @@ #include "catalog/pg_auth_members.h" #include "catalog/pg_authid.h" #include "catalog/pg_cast.h" +#include "catalog/pg_colenckey.h" #include "catalog/pg_collation.h" +#include "catalog/pg_colmasterkey.h" #include "catalog/pg_constraint.h" #include "catalog/pg_conversion.h" #include "catalog/pg_database.h" @@ -267,6 +269,24 @@ static const struct cachedesc cacheinfo[] = { }, 256 }, + { + ColumnEncKeyRelationId, /* CEKNAME */ + ColumnEncKeyNameIndexId, + 1, + { + Anum_pg_colenckey_cekname, + }, + 8 + }, + { + ColumnEncKeyRelationId, /* CEKOID */ + ColumnEncKeyOidIndexId, + 1, + { + Anum_pg_colenckey_oid, + }, + 8 + }, {OperatorClassRelationId, /* CLAAMNAMENSP */ OpclassAmNameNspIndexId, 3, @@ -289,6 +309,15 @@ static const struct cachedesc cacheinfo[] = { }, 8 }, + { + ColumnMasterKeyRelationId, /* CMKOID */ + ColumnMasterKeyOidIndexId, + 1, + { + Anum_pg_colmasterkey_oid, + }, + 8 + }, {CollationRelationId, /* COLLNAMEENCNSP */ CollationNameEncNspIndexId, 3, diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 7cc9c72e49..49889b69e5 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -8068,6 +8068,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) int i_typstorage; int i_attidentity; int i_attgenerated; + int i_attcekname; int i_attisdropped; int i_attlen; int i_attalign; @@ -8177,17 +8178,24 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) if (fout->remoteVersion >= 120000) appendPQExpBufferStr(q, - "a.attgenerated\n"); + "a.attgenerated,\n"); else appendPQExpBufferStr(q, - "'' AS attgenerated\n"); + "'' AS attgenerated,\n"); + + if (fout->remoteVersion >= 150000) + appendPQExpBufferStr(q, + "(SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek ) AS attcekname\n"); + else + appendPQExpBufferStr(q, + "NULL AS attcekname\n"); /* need left join to pg_type to not fail on dropped columns ... */ appendPQExpBuffer(q, "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n" "JOIN pg_catalog.pg_attribute a ON (src.tbloid = a.attrelid) " "LEFT JOIN pg_catalog.pg_type t " - "ON (a.atttypid = t.oid)\n" + "ON (CASE WHEN a.attrealtypid <> 0 THEN a.attrealtypid ELSE a.atttypid END = t.oid)\n" "WHERE a.attnum > 0::pg_catalog.int2\n" "ORDER BY a.attrelid, a.attnum", tbloids->data); @@ -8206,6 +8214,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) i_typstorage = PQfnumber(res, "typstorage"); i_attidentity = PQfnumber(res, "attidentity"); i_attgenerated = PQfnumber(res, "attgenerated"); + i_attcekname = PQfnumber(res, "attcekname"); i_attisdropped = PQfnumber(res, "attisdropped"); i_attlen = PQfnumber(res, "attlen"); i_attalign = PQfnumber(res, "attalign"); @@ -8267,6 +8276,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->typstorage = (char *) pg_malloc(numatts * sizeof(char)); tbinfo->attidentity = (char *) pg_malloc(numatts * sizeof(char)); tbinfo->attgenerated = (char *) pg_malloc(numatts * sizeof(char)); + tbinfo->attcekname = (char **) pg_malloc(numatts * sizeof(char *)); tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool)); tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int)); tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char)); @@ -8295,6 +8305,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables) tbinfo->attidentity[j] = *(PQgetvalue(res, r, i_attidentity)); tbinfo->attgenerated[j] = *(PQgetvalue(res, r, i_attgenerated)); tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS); + if (!PQgetisnull(res, r, i_attcekname)) + tbinfo->attcekname[j] = pg_strdup(PQgetvalue(res, r, i_attcekname)); + else + tbinfo->attcekname[j] = NULL; tbinfo->attisdropped[j] = (PQgetvalue(res, r, i_attisdropped)[0] == 't'); tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen)); tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign)); @@ -15265,6 +15279,12 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo) tbinfo->atttypnames[j]); } + if (tbinfo->attcekname[j]) + { + appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s)", + fmtId(tbinfo->attcekname[j])); + } + if (print_default) { if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED) diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 1d21c2906f..2c176ed13c 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -333,6 +333,7 @@ typedef struct _tableInfo bool *attisdropped; /* true if attr is dropped; don't dump it */ char *attidentity; char *attgenerated; + char **attcekname; int *attlen; /* attribute length, used by binary_upgrade */ char *attalign; /* attribute align, used by binary_upgrade */ bool *attislocal; /* true if attr has local definition */ diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index b51d28780b..816fd1ccf8 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -98,6 +98,7 @@ static backslashResult process_command_g_options(char *first_option, bool active_branch, const char *cmd); static backslashResult exec_command_gdesc(PsqlScanState scan_state, bool active_branch); +static backslashResult exec_command_gencr(PsqlScanState scan_state, bool active_branch); static backslashResult exec_command_getenv(PsqlScanState scan_state, bool active_branch, const char *cmd); static backslashResult exec_command_gexec(PsqlScanState scan_state, bool active_branch); @@ -350,6 +351,8 @@ exec_command(const char *cmd, status = exec_command_g(scan_state, active_branch, cmd); else if (strcmp(cmd, "gdesc") == 0) status = exec_command_gdesc(scan_state, active_branch); + else if (strcmp(cmd, "gencr") == 0) + status = exec_command_gencr(scan_state, active_branch); else if (strcmp(cmd, "getenv") == 0) status = exec_command_getenv(scan_state, active_branch, cmd); else if (strcmp(cmd, "gexec") == 0) @@ -1499,6 +1502,34 @@ exec_command_gdesc(PsqlScanState scan_state, bool active_branch) return status; } +/* + * \gencr -- send query, with support for parameter encryption + */ +static backslashResult +exec_command_gencr(PsqlScanState scan_state, bool active_branch) +{ + backslashResult status = PSQL_CMD_SKIP_LINE; + char *ap; + + pset.num_params = 0; + pset.params = NULL; + while ((ap = psql_scan_slash_option(scan_state, + OT_NORMAL, NULL, true)) != NULL) + { + pset.num_params++; + pset.params = pg_realloc(pset.params, pset.num_params * sizeof(char *)); + pset.params[pset.num_params - 1] = ap; + } + + if (active_branch) + { + pset.gencr_flag = true; + status = PSQL_CMD_SEND; + } + + return status; +} + /* * \getenv -- set variable from environment variable */ diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c index 974959c595..aec52a1d09 100644 --- a/src/bin/psql/common.c +++ b/src/bin/psql/common.c @@ -1285,6 +1285,14 @@ SendQuery(const char *query) /* reset \gdesc trigger */ pset.gdesc_flag = false; + /* reset \gencr trigger */ + pset.gencr_flag = false; + for (int i = 0; i < pset.num_params; i++) + pg_free(pset.params[i]); + pg_free(pset.params); + pset.params = NULL; + pset.num_params = 0; + /* reset \gexec trigger */ pset.gexec_flag = false; @@ -1449,7 +1457,35 @@ ExecQueryAndProcessResults(const char *query, double *elapsed_msec, bool *svpt_g if (timing) INSTR_TIME_SET_CURRENT(before); - success = PQsendQuery(pset.db, query); + if (pset.gencr_flag) + { + PGresult *res1, *res2; + + res1 = PQprepare(pset.db, "", query, pset.num_params, NULL); + if (PQresultStatus(res1) != PGRES_COMMAND_OK) + { + pg_log_info("%s", PQerrorMessage(pset.db)); + ClearOrSaveResult(res1); + return -1; + } + PQclear(res1); + + res2 = PQdescribePrepared(pset.db, ""); + if (PQresultStatus(res2) != PGRES_COMMAND_OK) + { + pg_log_info("%s", PQerrorMessage(pset.db)); + ClearOrSaveResult(res2); + return -1; + } + + success = PQsendQueryPrepared2(pset.db, "", pset.num_params, (const char *const*) pset.params, NULL, NULL, NULL, 0, res2); + + PQclear(res2); + } + else + { + success = PQsendQuery(pset.db, query); + } if (!success) { diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index d1ae699171..b5fc3d66c8 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -1498,7 +1498,7 @@ describeOneTableDetails(const char *schemaname, bool printTableInitialized = false; int i; char *view_def = NULL; - char *headers[12]; + char *headers[13]; PQExpBufferData title; PQExpBufferData tmpbuf; int cols; @@ -1514,6 +1514,7 @@ describeOneTableDetails(const char *schemaname, fdwopts_col = -1, attstorage_col = -1, attcompression_col = -1, + attcekname_col = -1, attstattarget_col = -1, attdescr_col = -1; int numrows; @@ -1813,7 +1814,7 @@ describeOneTableDetails(const char *schemaname, cols = 0; printfPQExpBuffer(&buf, "SELECT a.attname"); attname_col = cols++; - appendPQExpBufferStr(&buf, ",\n pg_catalog.format_type(a.atttypid, a.atttypmod)"); + appendPQExpBufferStr(&buf, ",\n pg_catalog.format_type(CASE WHEN a.attrealtypid <> 0 THEN a.attrealtypid ELSE a.atttypid END, a.atttypmod)"); atttype_col = cols++; if (show_column_details) @@ -1827,7 +1828,7 @@ describeOneTableDetails(const char *schemaname, attrdef_col = cols++; attnotnull_col = cols++; appendPQExpBufferStr(&buf, ",\n (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n" - " WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) AS attcollation"); + " WHERE c.oid = a.attcollation AND t.oid = (CASE WHEN a.attrealtypid <> 0 THEN a.attrealtypid ELSE a.atttypid END) AND a.attcollation <> t.typcollation) AS attcollation"); attcoll_col = cols++; if (pset.sversion >= 100000) appendPQExpBufferStr(&buf, ",\n a.attidentity"); @@ -1878,6 +1879,15 @@ describeOneTableDetails(const char *schemaname, attcompression_col = cols++; } + /* encryption info */ + if (pset.sversion >= 150000 && + !pset.hide_column_encryption && + (tableinfo.relkind == RELKIND_RELATION)) + { + appendPQExpBufferStr(&buf, ",\n (SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek) AS attcekname"); + attcekname_col = cols++; + } + /* stats target, if relevant to relkind */ if (tableinfo.relkind == RELKIND_RELATION || tableinfo.relkind == RELKIND_INDEX || @@ -2001,6 +2011,8 @@ describeOneTableDetails(const char *schemaname, headers[cols++] = gettext_noop("Storage"); if (attcompression_col >= 0) headers[cols++] = gettext_noop("Compression"); + if (attcekname_col >= 0) + headers[cols++] = gettext_noop("Encryption"); if (attstattarget_col >= 0) headers[cols++] = gettext_noop("Stats target"); if (attdescr_col >= 0) @@ -2093,6 +2105,17 @@ describeOneTableDetails(const char *schemaname, false, false); } + /* Column encryption */ + if (attcekname_col >= 0) + { + if (!PQgetisnull(res, i, attcekname_col)) + printTableAddCell(&cont, PQgetvalue(res, i, attcekname_col), + false, false); + else + printTableAddCell(&cont, "", + false, false); + } + /* Statistics target, if the relkind supports this feature */ if (attstattarget_col >= 0) printTableAddCell(&cont, PQgetvalue(res, i, attstattarget_col), diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index f8ce1a0706..9f4fce26fd 100644 --- a/src/bin/psql/help.c +++ b/src/bin/psql/help.c @@ -195,6 +195,7 @@ slashUsage(unsigned short int pager) HELP0(" \\g [(OPTIONS)] [FILE] execute query (and send result to file or |pipe);\n" " \\g with no arguments is equivalent to a semicolon\n"); HELP0(" \\gdesc describe result of query, without executing it\n"); + HELP0(" \\gencr [PARAM]... execute query, with support for parameter encryption\n"); HELP0(" \\gexec execute query, then execute each value in its result\n"); HELP0(" \\gset [PREFIX] execute query and store result in psql variables\n"); HELP0(" \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n"); @@ -412,6 +413,8 @@ helpVariables(unsigned short int pager) " true if last query failed, else false\n"); HELP0(" FETCH_COUNT\n" " the number of result rows to fetch and display at a time (0 = unlimited)\n"); + HELP0(" HIDE_COLUMN_ENCRYPTION\n" + " if set, column encryption details are not displayed\n"); HELP0(" HIDE_TABLEAM\n" " if set, table access methods are not displayed\n"); HELP0(" HIDE_TOAST_COMPRESSION\n" diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h index 2399cffa3f..636729df1d 100644 --- a/src/bin/psql/settings.h +++ b/src/bin/psql/settings.h @@ -95,6 +95,10 @@ typedef struct _psqlSettings char *gset_prefix; /* one-shot prefix argument for \gset */ bool gdesc_flag; /* one-shot request to describe query result */ + bool gencr_flag; /* one-shot request to send query with support + * for parameter encryption */ + int num_params; /* number of query parameters */ + char **params; /* query parameters */ bool gexec_flag; /* one-shot request to execute query result */ bool crosstab_flag; /* one-shot request to crosstab result */ char *ctv_args[4]; /* \crosstabview arguments */ @@ -134,6 +138,7 @@ typedef struct _psqlSettings bool quiet; bool singleline; bool singlestep; + bool hide_column_encryption; bool hide_compression; bool hide_tableam; int fetch_count; diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c index 7c2f555f15..c955222a50 100644 --- a/src/bin/psql/startup.c +++ b/src/bin/psql/startup.c @@ -1188,6 +1188,13 @@ hide_compression_hook(const char *newval) &pset.hide_compression); } +static bool +hide_column_encryption_hook(const char *newval) +{ + return ParseVariableBool(newval, "HIDE_COLUMN_ENCRYPTION", + &pset.hide_column_encryption); +} + static bool hide_tableam_hook(const char *newval) { @@ -1259,6 +1266,9 @@ EstablishVariableSpace(void) SetVariableHooks(pset.vars, "SHOW_CONTEXT", show_context_substitute_hook, show_context_hook); + SetVariableHooks(pset.vars, "HIDE_COLUMN_ENCRYPTION", + bool_substitute_hook, + hide_column_encryption_hook); SetVariableHooks(pset.vars, "HIDE_TOAST_COMPRESSION", bool_substitute_hook, hide_compression_hook); diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index bd44a1d55d..b4ae83a551 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -1692,7 +1692,7 @@ psql_completion(const char *text, int start, int end) "\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding", "\\endif", "\\errverbose", "\\ev", "\\f", - "\\g", "\\gdesc", "\\getenv", "\\gexec", "\\gset", "\\gx", + "\\g", "\\gdesc", "\\gencr", "\\getenv", "\\gexec", "\\gset", "\\gx", "\\help", "\\html", "\\if", "\\include", "\\include_relative", "\\ir", "\\list", "\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink", diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index b5fd72a4ac..5a065eb80b 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -54,6 +54,7 @@ endif ifeq ($(with_ssl),openssl) OBJS += \ + fe-encrypt-openssl.o \ fe-secure-openssl.o endif diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index e8bcc88370..fabad38ac0 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -186,3 +186,7 @@ PQpipelineStatus 183 PQsetTraceFlags 184 PQmblenBounded 185 PQsendFlushRequest 186 +PQexecPrepared2 187 +PQsendQueryPrepared2 188 +PQfisencrypted 189 +PQparamisencrypted 190 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 6e936bbff3..654768586f 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -344,6 +344,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */ offsetof(struct pg_conn, target_session_attrs)}, + {"cmklookup", "PGCMKLOOKUP", "", NULL, + "CMK-Lookup", "", 64, + offsetof(struct pg_conn, cmklookup)}, + /* Terminating entry --- MUST BE LAST */ {NULL, NULL, NULL, NULL, NULL, NULL, 0} @@ -4155,6 +4159,22 @@ freePGconn(PGconn *conn) free(conn->gsslib); if (conn->connip) free(conn->connip); + free(conn->cmklookup); + for (int i = 0; i < conn->ncmks; i++) + { + free(conn->cmks[i].cmkname); + free(conn->cmks[i].cmkrealm); + } + free(conn->cmks); + for (int i = 0; i < conn->nceks; i++) + { + if (conn->ceks[i].cekdata) + { + explicit_bzero(conn->ceks[i].cekdata, conn->ceks[i].cekdatalen); + free(conn->ceks[i].cekdata); + } + } + free(conn->ceks); /* Note that conn->Pfdebug is not ours to close or free */ if (conn->write_err_msg) free(conn->write_err_msg); diff --git a/src/interfaces/libpq/fe-encrypt-openssl.c b/src/interfaces/libpq/fe-encrypt-openssl.c new file mode 100644 index 0000000000..7ebf963a7c --- /dev/null +++ b/src/interfaces/libpq/fe-encrypt-openssl.c @@ -0,0 +1,753 @@ +/*------------------------------------------------------------------------- + * + * fe-encrypt-openssl.c + * encryption support using OpenSSL + * + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/interfaces/libpq/fe-encrypt-openssl.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include "fe-encrypt.h" +#include "libpq-int.h" + +#include "catalog/pg_colenckey.h" +#include "port/pg_bswap.h" + +#include + + +#ifdef TEST_ENCRYPT + +/* + * Test data from + * + */ + +/* + * The different test cases just use different prefixes of K, so one constant + * is enough here. + */ +static const unsigned char K[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, +}; + +static const unsigned char P[] = { + 0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, + 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, + 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65, +}; + +static const unsigned char test_IV[] = { + 0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04, +}; + +static const unsigned char test_A[] = { + 0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, + 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, + 0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73, +}; + + +#define libpq_gettext(x) (x) + +#endif /* TEST_ENCRYPT */ + + +unsigned char * +decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg, + int fromlen, const unsigned char *from, + int *tolen) +{ + const EVP_MD *md = NULL; + EVP_PKEY *key = NULL; + RSA *rsa = NULL; + EVP_PKEY_CTX *ctx = NULL; + unsigned char *out = NULL; + size_t outlen; + + switch (cmkalg) + { + case PG_CMK_RSAES_OAEP_SHA_1: + md = EVP_sha1(); + break; + case PG_CMK_RSAES_OAEP_SHA_256: + md = EVP_sha256(); + break; + default: + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("unsupported CMK algorithm ID: %d\n"), cmkalg); + goto fail; + } + + rsa = RSA_new(); + if (!rsa) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not allocate RSA structure: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + rsa = PEM_read_RSAPrivateKey(fp, &rsa, NULL, NULL); + if (!rsa) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not read RSA private key: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + key = EVP_PKEY_new(); + if (!key) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not allocate private key structure: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + if (!EVP_PKEY_assign_RSA(key, rsa)) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not assign private key: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + ctx = EVP_PKEY_CTX_new(key, NULL); + if (!ctx) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not allocate public key algorithm context: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + if (EVP_PKEY_decrypt_init(ctx) <= 0) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("decryption initialization failed: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0 || + EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, md) <= 0 || + EVP_PKEY_CTX_set_rsa_oaep_md(ctx, md) <= 0) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not set RSA parameter: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("RSA decryption failed: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + out = malloc(outlen); + if (!out) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("out of memory\n")); + goto fail; + } + + if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("RSA decryption failed: %s\n"), + ERR_reason_error_string(ERR_get_error())); + free(out); + out = NULL; + goto fail; + } + + *tolen = outlen; + +fail: + EVP_PKEY_CTX_free(ctx); + EVP_PKEY_free(key); + + return out; +} + +static const EVP_CIPHER * +pg_cekalg_to_openssl_cipher(int cekalg) +{ + const EVP_CIPHER *cipher; + + switch (cekalg) + { + case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256: + cipher = EVP_aes_128_cbc(); + break; + case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384: + cipher = EVP_aes_192_cbc(); + break; + case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384: + case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512: + cipher = EVP_aes_256_cbc(); + break; + default: + cipher = NULL; + } + + return cipher; +} + +static const EVP_MD * +pg_cekalg_to_openssl_md(int cekalg) +{ + const EVP_MD *md; + + switch (cekalg) + { + case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256: + md = EVP_sha256(); + break; + case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384: + case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384: + md = EVP_sha384(); + break; + case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512: + md = EVP_sha512(); + break; + default: + md = NULL; + } + + return md; +} + +static int +md_key_length(const EVP_MD *md) +{ + if (md == EVP_sha256()) + return 16; + else if (md == EVP_sha384()) + return 24; + else if (md == EVP_sha512()) + return 32; + else + return -1; +} + +static int +md_hash_length(const EVP_MD *md) +{ + if (md == EVP_sha256()) + return 32; + else if (md == EVP_sha384()) + return 48; + else if (md == EVP_sha512()) + return 64; + else + return -1; +} + +#ifndef TEST_ENCRYPT +#define PG_AD_LEN 4 +#else +#define PG_AD_LEN sizeof(test_A) +#endif + +static bool +get_message_auth_tag(const EVP_MD *md, + const unsigned char *mac_key, int mac_key_len, + const unsigned char *encr, int encrlen, + int cekalg, + unsigned char *md_value, size_t *md_len_p, + const char **errmsgp) +{ + static char msgbuf[1024]; + EVP_MD_CTX *evp_md_ctx = NULL; + EVP_PKEY *pkey = NULL; + size_t bufsize; + unsigned char *buf = NULL; + bool result = false; + + evp_md_ctx = EVP_MD_CTX_new(); + + pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, mac_key, mac_key_len); + if (!pkey) + { + snprintf(msgbuf, sizeof(msgbuf), + libpq_gettext("could not allocate key for HMAC: %s"), + ERR_reason_error_string(ERR_get_error())); + *errmsgp = msgbuf; + goto fail; + } + + bufsize = PG_AD_LEN + encrlen + sizeof(int64); + buf = malloc(bufsize); + if (!buf) + { + *errmsgp = libpq_gettext("out out memory"); + goto fail; + } +#ifndef TEST_ENCRYPT + buf[0] = 'P'; + buf[1] = 'G'; + *(int16 *) (buf + 2) = pg_hton16(cekalg); +#else + memcpy(buf, test_A, sizeof(test_A)); +#endif + memcpy(buf + PG_AD_LEN, encr, encrlen); + *(int64 *) (buf + PG_AD_LEN + encrlen) = pg_hton64(PG_AD_LEN * 8); + + if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey)) + { + snprintf(msgbuf, sizeof(msgbuf), + libpq_gettext("digest initialization failed: %s"), + ERR_reason_error_string(ERR_get_error())); + *errmsgp = msgbuf; + goto fail; + } + + if (!EVP_DigestSignUpdate(evp_md_ctx, buf, bufsize)) + { + snprintf(msgbuf, sizeof(msgbuf), + libpq_gettext("digest signing failed: %s"), + ERR_reason_error_string(ERR_get_error())); + *errmsgp = msgbuf; + goto fail; + } + + if (!EVP_DigestSignFinal(evp_md_ctx, md_value, md_len_p)) + { + snprintf(msgbuf, sizeof(msgbuf), + libpq_gettext("digest signing failed: %s"), + ERR_reason_error_string(ERR_get_error())); + *errmsgp = msgbuf; + goto fail; + } + Assert(*md_len_p == md_hash_length(md)); + + /* truncate output to half the length, per spec */ + *md_len_p /= 2; + + result = true; +fail: + free(buf); + EVP_PKEY_free(pkey); + EVP_MD_CTX_free(evp_md_ctx); + return result; +} + +unsigned char * +decrypt_value(PGresult *res, const PGCEK *cek, int cekalg, const unsigned char *input, int inputlen, const char **errmsgp) +{ + static char msgbuf[1024]; + + const unsigned char *iv = NULL; + size_t ivlen; + + const EVP_CIPHER *cipher; + const EVP_MD *md; + EVP_CIPHER_CTX *evp_cipher_ctx = NULL; + int enc_key_len; + int mac_key_len; + int key_len; + const unsigned char *enc_key; + const unsigned char *mac_key; + unsigned char md_value[EVP_MAX_MD_SIZE]; + size_t md_len = sizeof(md_value); + size_t bufsize; + unsigned char *buf = NULL; + unsigned char *decr; + int decrlen, + decrlen2; + + unsigned char *result = NULL; + + cipher = pg_cekalg_to_openssl_cipher(cekalg); + if (!cipher) + { + snprintf(msgbuf, sizeof(msgbuf), + libpq_gettext("unrecognized encryption algorithm identifier: %d"), cekalg); + *errmsgp = msgbuf; + goto fail; + } + + md = pg_cekalg_to_openssl_md(cekalg); + if (!md) + { + snprintf(msgbuf, sizeof(msgbuf), + libpq_gettext("unrecognized digest algorithm identifier: %d"), cekalg); + *errmsgp = msgbuf; + goto fail; + } + + evp_cipher_ctx = EVP_CIPHER_CTX_new(); + + if (!EVP_DecryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL)) + { + snprintf(msgbuf, sizeof(msgbuf), + libpq_gettext("decryption initialization failed: %s"), + ERR_reason_error_string(ERR_get_error())); + *errmsgp = msgbuf; + goto fail; + } + + enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx); + mac_key_len = md_key_length(md); + key_len = mac_key_len + enc_key_len; + + if (cek->cekdatalen != key_len) + { + snprintf(msgbuf, sizeof(msgbuf), + libpq_gettext("column encryption key has wrong length for algorithm (has: %zu, required: %d)"), + cek->cekdatalen, key_len); + *errmsgp = msgbuf; + goto fail; + } + + enc_key = cek->cekdata + mac_key_len; + mac_key = cek->cekdata; + + if (!get_message_auth_tag(md, mac_key, mac_key_len, + input, inputlen - (md_hash_length(md) / 2), + cekalg, + md_value, &md_len, + errmsgp)) + { + goto fail; + } + + if (memcmp(input + (inputlen - md_len), md_value, md_len) != 0) + { + *errmsgp = libpq_gettext("MAC mismatch"); + goto fail; + } + + ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx); + iv = input; + input += ivlen; + inputlen -= ivlen; + if (!EVP_DecryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv)) + { + snprintf(msgbuf, sizeof(msgbuf), + libpq_gettext("decryption initialization failed: %s"), + ERR_reason_error_string(ERR_get_error())); + *errmsgp = msgbuf; + goto fail; + } + + bufsize = inputlen + EVP_CIPHER_CTX_block_size(evp_cipher_ctx) + 1; +#ifndef TEST_ENCRYPT + buf = pqResultAlloc(res, bufsize, false); +#else + buf = malloc(bufsize); +#endif + if (!buf) + { + *errmsgp = libpq_gettext("out out memory"); + goto fail; + } + decr = buf; + if (!EVP_DecryptUpdate(evp_cipher_ctx, decr, &decrlen, input, inputlen - md_len)) + { + snprintf(msgbuf, sizeof(msgbuf), + libpq_gettext("decryption failed: %s"), + ERR_reason_error_string(ERR_get_error())); + *errmsgp = msgbuf; + goto fail; + } + if (!EVP_DecryptFinal_ex(evp_cipher_ctx, decr + decrlen, &decrlen2)) + { + snprintf(msgbuf, sizeof(msgbuf), + libpq_gettext("decryption failed: %s"), + ERR_reason_error_string(ERR_get_error())); + *errmsgp = msgbuf; + goto fail; + } + decrlen += decrlen2; + Assert(decrlen < bufsize); + decr[decrlen] = '\0'; + result = decr; + +fail: + EVP_CIPHER_CTX_free(evp_cipher_ctx); + + return result; +} + +#ifndef TEST_ENCRYPT +static bool +make_siv(PGconn *conn, + unsigned char *iv, size_t ivlen, + const EVP_MD *md, + const unsigned char *iv_key, int iv_key_len, + const unsigned char *plaintext, int plaintext_len) +{ + EVP_MD_CTX *evp_md_ctx = NULL; + EVP_PKEY *pkey = NULL; + bool result = false; + + evp_md_ctx = EVP_MD_CTX_new(); + + pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, iv_key, iv_key_len); + if (!pkey) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not allocate key for HMAC: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey)) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("digest initialization failed: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len)) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("digest signing failed: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + if (!EVP_DigestSignFinal(evp_md_ctx, iv, &ivlen)) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("digest signing failed: %s"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + Assert(ivlen == md_hash_length(md)); + + result = true; +fail: + EVP_PKEY_free(pkey); + EVP_MD_CTX_free(evp_md_ctx); + return result; +} +#endif /* TEST_ENCRYPT */ + +unsigned char * +encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg, const unsigned char *value, int *nbytesp, bool enc_det) +{ + int nbytes = *nbytesp; + unsigned char iv[EVP_MAX_IV_LENGTH]; + size_t ivlen; + const EVP_CIPHER *cipher; + const EVP_MD *md; + EVP_CIPHER_CTX *evp_cipher_ctx = NULL; + int enc_key_len; + int mac_key_len; + int key_len; + const unsigned char *enc_key; + const unsigned char *mac_key; + size_t bufsize; + unsigned char *buf = NULL; + unsigned char *encr; + int encrlen, + encrlen2; + + const char *errmsg; + unsigned char md_value[EVP_MAX_MD_SIZE]; + size_t md_len = sizeof(md_value); + size_t buf2size; + unsigned char *buf2 = NULL; + + unsigned char *result = NULL; + + cipher = pg_cekalg_to_openssl_cipher(cekalg); + if (!cipher) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("unrecognized encryption algorithm identifier: %d\n"), cekalg); + goto fail; + } + + md = pg_cekalg_to_openssl_md(cekalg); + if (!md) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("unrecognized digest algorithm identifier: %d\n"), cekalg); + goto fail; + } + + evp_cipher_ctx = EVP_CIPHER_CTX_new(); + + if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL)) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("encryption initialization failed: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx); + mac_key_len = md_key_length(md); + key_len = mac_key_len + enc_key_len; + + if (cek->cekdatalen != key_len) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("column encryption key has wrong length for algorithm (has: %zu, required: %d)\n"), + cek->cekdatalen, key_len); + goto fail; + } + + enc_key = cek->cekdata + mac_key_len; + mac_key = cek->cekdata; + + ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx); + Assert(ivlen <= sizeof(iv)); + if (enc_det) +#ifndef TEST_ENCRYPT + make_siv(conn, iv, ivlen, md, mac_key, mac_key_len, value, nbytes); +#else + memcpy(iv, test_IV, ivlen); +#endif + else + pg_strong_random(iv, ivlen); + if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv)) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("encryption initialization failed: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + + bufsize = ivlen + (nbytes + 2 * EVP_CIPHER_CTX_block_size(evp_cipher_ctx) - 1); + buf = malloc(bufsize); + if (!buf) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("out of memory\n")); + goto fail; + } + memcpy(buf, iv, ivlen); + encr = buf + ivlen; + if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes)) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("encryption failed: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2)) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("encryption failed: %s\n"), + ERR_reason_error_string(ERR_get_error())); + goto fail; + } + encrlen += encrlen2; + + encr -= ivlen; + encrlen += ivlen; + + Assert(encrlen <= bufsize); + + if (!get_message_auth_tag(md, mac_key, mac_key_len, + encr, encrlen, + cekalg, + md_value, &md_len, + &errmsg)) + { + appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg); + goto fail; + } + + buf2size = encrlen + md_len; + buf2 = malloc(buf2size); + if (!buf2) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("out of memory\n")); + goto fail; + } + memcpy(buf2, encr, encrlen); + memcpy(buf2 + encrlen, md_value, md_len); + + result = buf2; + nbytes = buf2size; + +fail: + free(buf); + EVP_CIPHER_CTX_free(evp_cipher_ctx); + + *nbytesp = nbytes; + return result; +} + + +/* + * Run test cases + */ +#ifdef TEST_ENCRYPT + +static void +debug_print_hex(const char *name, const unsigned char *val, int len) +{ + printf("%s =", name); + for (int i = 0; i < len; i++) + { + if (i % 16 == 0) + printf("\n"); + else + printf(" "); + printf("%02x", val[i]); + } + printf("\n"); +} + +static void +test_case(int alg, const unsigned char *K, size_t K_len, const unsigned char *P, size_t P_len) +{ + unsigned char *C; + int nbytes; + PGCEK cek; + + nbytes = P_len; + cek.cekdata = unconstify(unsigned char *, K); + cek.cekdatalen = K_len; + + C = encrypt_value(NULL, &cek, alg, P, &nbytes, true); + debug_print_hex("C", C, nbytes); +} + +int +main(int argc, char **argv) +{ + printf("5.1\n"); + test_case(PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256, K, 32, P, sizeof(P)); + printf("5.2\n"); + test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, P, sizeof(P)); + printf("5.3\n"); + test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, P, sizeof(P)); + printf("5.4\n"); + test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, P, sizeof(P)); + + return 0; +} + +#endif /* TEST_ENCRYPT */ diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h new file mode 100644 index 0000000000..b0093dc527 --- /dev/null +++ b/src/interfaces/libpq/fe-encrypt.h @@ -0,0 +1,33 @@ +/*------------------------------------------------------------------------- + * + * fe-encrypt.h + * + * encryption support + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq/fe-encrypt.h + * + *------------------------------------------------------------------------- + */ + +#ifndef FE_ENCRYPT_H +#define FE_ENCRYPT_H + +#include "libpq-fe.h" +#include "libpq-int.h" + +extern unsigned char *decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg, + int fromlen, const unsigned char *from, + int *tolen); + +extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg, + const unsigned char *input, int inputlen, + const char **errmsgp); + +extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg, + const unsigned char *value, int *nbytesp, bool enc_det); + +#endif /* FE_ENCRYPT_H */ diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c index 919cf5741d..db8dde1cb0 100644 --- a/src/interfaces/libpq/fe-exec.c +++ b/src/interfaces/libpq/fe-exec.c @@ -24,6 +24,9 @@ #include #endif +#include "catalog/pg_colenckey.h" +#include "common/base64.h" +#include "fe-encrypt.h" #include "libpq-fe.h" #include "libpq-int.h" #include "mb/pg_wchar.h" @@ -72,7 +75,9 @@ static int PQsendQueryGuts(PGconn *conn, const char *const *paramValues, const int *paramLengths, const int *paramFormats, - int resultFormat); + const int *paramForceColumnEncryptions, + int resultFormat, + PGresult *paramDesc); static void parseInput(PGconn *conn); static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype); static bool PQexecStart(PGconn *conn); @@ -1189,6 +1194,381 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value) } } +int +pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname, + const char *keyrealm) +{ + char *keyname_copy; + char *keyrealm_copy; + bool found; + + keyname_copy = strdup(keyname); + if (!keyname_copy) + return EOF; + keyrealm_copy = strdup(keyrealm); + if (!keyrealm_copy) + { + free(keyname_copy); + return EOF; + } + + found = false; + for (int i = 0; i < conn->ncmks; i++) + { + struct pg_cmk *checkcmk = &conn->cmks[i]; + + /* replace existing? */ + if (checkcmk->cmkid == keyid) + { + free(checkcmk->cmkname); + free(checkcmk->cmkrealm); + checkcmk->cmkname = keyname_copy; + checkcmk->cmkrealm = keyrealm_copy; + found = true; + break; + } + } + + /* append new? */ + if (!found) + { + int newncmks; + struct pg_cmk *newcmks; + struct pg_cmk *newcmk; + + newncmks = conn->ncmks + 1; + if (newncmks <= 0) + return EOF; + newcmks = realloc(conn->cmks, newncmks * sizeof(struct pg_cmk)); + if (!newcmks) + { + free(keyname_copy); + free(keyrealm_copy); + return EOF; + } + + newcmk = &newcmks[newncmks - 1]; + newcmk->cmkid = keyid; + newcmk->cmkname = keyname_copy; + newcmk->cmkrealm = keyrealm_copy; + + conn->ncmks = newncmks; + conn->cmks = newcmks; + } + + return 0; +} + +/* + * Replace placeholders in input string. Return value malloc'ed. + */ +static char * +replace_cmk_placeholders(const char *in, const char *cmkname, const char *cmkrealm, int cmkalg, const char *b64data) +{ + PQExpBufferData buf; + + initPQExpBuffer(&buf); + + for (const char *p = in; *p; p++) + { + if (p[0] == '%') + { + switch (p[1]) + { + case 'a': + { + const char *s; + + switch (cmkalg) + { + case PG_CMK_RSAES_OAEP_SHA_1: + s = "RSAES_OAEP_SHA_1"; + break; + case PG_CMK_RSAES_OAEP_SHA_256: + s = "RSAES_OAEP_SHA_256"; + break; + default: + s = "INVALID"; + } + appendPQExpBufferStr(&buf, s); + } + p++; + break; + case 'b': + appendPQExpBufferStr(&buf, b64data); + p++; + break; + case 'k': + appendPQExpBufferStr(&buf, cmkname); + p++; + break; + case 'r': + appendPQExpBufferStr(&buf, cmkrealm); + p++; + break; + default: + appendPQExpBufferChar(&buf, p[0]); + } + } + else + appendPQExpBufferChar(&buf, p[0]); + } + + return buf.data; +} + +#ifndef USE_SSL +/* + * Dummy implementation for non-SSL builds + */ +unsigned char * +decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg, + int fromlen, const unsigned char *from, + int *tolen) +{ + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("column encryption not supported by this build\n")); + return NULL; +} +#endif + +/* + * Decrypt a CEK using the given CMK. The ciphertext is passed in + * "from" and "fromlen". Return the decrypted value in a malloc'ed area, its + * length via "tolen". Return NULL on error; add error messages directly to + * "conn". + */ +static unsigned char * +decrypt_cek(PGconn *conn, const PGCMK *cmk, int cmkalg, + int fromlen, const unsigned char *from, + int *tolen) +{ + char *cmklookup; + bool found = false; + unsigned char *result = NULL; + + cmklookup = strdup(conn->cmklookup ? conn->cmklookup : ""); + + if (!cmklookup) + { + appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n")); + return NULL; + } + + /* + * Analyze semicolon-separated list + */ + for (char *s = strtok(cmklookup, ";"); s; s = strtok(NULL, ";")) + { + char *sep; + + /* split found token at '=' */ + sep = strchr(s, '='); + if (!sep) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("syntax error in CMK lookup specification, missing \"%c\": %s\n"), '=', s); + break; + } + + /* matching realm? */ + if (strncmp(s, "*", sep - s) == 0 || strncmp(s, cmk->cmkrealm , sep - s) == 0) + { + char *sep2; + + found = true; + + sep2 = strchr(sep, ':'); + if (!sep2) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("syntax error in CMK lookup specification, missing \"%c\": %s\n"), ':', s); + goto fail; + } + + if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0) + { + char *cmkfilename; + FILE *fp; + + cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID"); + fp = fopen(cmkfilename, "rb"); + if (fp) + { + result = decrypt_cek_from_file(conn, fp, cmkalg, fromlen, from, tolen); + fclose(fp); + } + else + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not open file \"%s\": %m\n"), cmkfilename); + free(cmkfilename); + goto fail; + } + free(cmkfilename); + } + else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0) + { + int enclen; + char *enc; + char *command; + FILE *fp; + char buf[4096]; + + enclen = pg_b64_enc_len(fromlen); + enc = malloc(enclen + 1); + if (!enc) + { + appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n")); + goto fail; + } + enclen = pg_b64_encode((const char *) from, fromlen, enc, enclen); + if (enclen < 0) + { + appendPQExpBuffer(&conn->errorMessage, libpq_gettext("base64 encoding failed\n")); + free(enc); + goto fail; + } + command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, enc); + free(enc); + fp = popen(command, "r"); + free(command); + if (!fp) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not run command \"%s\": %m\n"), command); + goto fail; + } + if (fgets(buf, sizeof(buf), fp)) + { + int linelen; + int declen; + char *dec; + + linelen = strlen(buf); + if (buf[linelen - 1] == '\n') + { + buf[linelen - 1] = '\0'; + linelen--; + } + declen = pg_b64_dec_len(linelen); + dec = malloc(declen); + if (!dec) + { + appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n")); + goto fail; + } + declen = pg_b64_decode(buf, linelen, dec, declen); + if (declen < 0) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("base64 decoding failed\n")); + free(dec); + goto fail; + } + result = (unsigned char *) dec; + *tolen = declen; + } + else + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not read from command: %m\n")); + pclose(fp); + goto fail; + } + pclose(fp); + } + else + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("CMK lookup scheme \"%s\" not recognized\n"), sep + 1); + goto fail; + } + } + } + + if (!found) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("no CMK lookup found for realm \"%s\"\n"), cmk->cmkrealm); + } + +fail: + free(cmklookup); + return result; +} + +int +pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, const unsigned char *value, int len) +{ + PGCMK *cmk = NULL; + unsigned char *plainval = NULL; + int plainvallen = 0; + bool found; + + for (int i = 0; i < conn->ncmks; i++) + { + if (conn->cmks[i].cmkid == cmkid) + { + cmk = &conn->cmks[i]; + break; + } + } + + if (!cmk) + return EOF; + + plainval = decrypt_cek(conn, cmk, cmkalg, len, value, &plainvallen); + if (!plainval) + return EOF; + + found = false; + for (int i = 0; i < conn->nceks; i++) + { + struct pg_cek *checkcek = &conn->ceks[i]; + + /* replace existing? */ + if (checkcek->cekid == keyid) + { + free(checkcek->cekdata); + checkcek->cekdata = plainval; + checkcek->cekdatalen = plainvallen; + found = true; + break; + } + } + + /* append new? */ + if (!found) + { + int newnceks; + struct pg_cek *newceks; + struct pg_cek *newcek; + + newnceks = conn->nceks + 1; + if (newnceks <= 0) + { + free(plainval); + return EOF; + } + newceks = realloc(conn->ceks, newnceks * sizeof(struct pg_cek)); + if (!newceks) + { + free(plainval); + return EOF; + } + + newcek = &newceks[newnceks - 1]; + newcek->cekid = keyid; + newcek->cekdata = plainval; + newcek->cekdatalen = plainvallen; + + conn->nceks = newnceks; + conn->ceks = newceks; + } + + return 0; +} /* * pqRowProcessor @@ -1254,9 +1634,58 @@ pqRowProcessor(PGconn *conn, const char **errmsgp) } else { - bool isbinary = (res->attDescs[i].format != 0); + bool isbinary = ((res->attDescs[i].format & 0x0F) != 0); char *val; + if (res->attDescs[i].cekid) + { +#ifdef USE_SSL + PGCEK *cek = NULL; + + for (int j = 0; j < conn->nceks; j++) + { + if (conn->ceks[j].cekid == res->attDescs[i].cekid) + { + cek = &conn->ceks[j]; + break; + } + } + if (!cek) + { + *errmsgp = libpq_gettext("column encryption key not found"); + goto fail; + } + + if (isbinary) + { + val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg, + (const unsigned char *) columns[i].value, clen, errmsgp); + } + else + { + unsigned char *buf; + unsigned char *enccolval; + size_t enccolvallen; + + buf = malloc(clen + 1); + memcpy(buf, columns[i].value, clen); + buf[clen] = '\0'; + enccolval = PQunescapeBytea(buf, &enccolvallen); + val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg, + enccolval, enccolvallen, errmsgp); + free(enccolval); + free(buf); + } + + if (val == NULL) + goto fail; +#else + *errmsgp = libpq_gettext("column encryption not supported by this build"); + goto fail; +#endif + } + else + { val = (char *) pqResultAlloc(res, clen + 1, isbinary); if (val == NULL) goto fail; @@ -1264,6 +1693,7 @@ pqRowProcessor(PGconn *conn, const char **errmsgp) /* copy and zero-terminate the data (even if it's binary) */ memcpy(val, columns[i].value, clen); val[clen] = '\0'; + } tup[i].len = clen; tup[i].value = val; @@ -1568,7 +1998,9 @@ PQsendQueryParams(PGconn *conn, paramValues, paramLengths, paramFormats, - resultFormat); + NULL, + resultFormat, + NULL); } /* @@ -1686,6 +2118,20 @@ PQsendQueryPrepared(PGconn *conn, const int *paramLengths, const int *paramFormats, int resultFormat) +{ + return PQsendQueryPrepared2(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, NULL, resultFormat, NULL); +} + +int +PQsendQueryPrepared2(PGconn *conn, + const char *stmtName, + int nParams, + const char *const *paramValues, + const int *paramLengths, + const int *paramFormats, + const int *paramForceColumnEncryptions, + int resultFormat, + PGresult *paramDesc) { if (!PQsendQueryStart(conn, true)) return 0; @@ -1713,7 +2159,9 @@ PQsendQueryPrepared(PGconn *conn, paramValues, paramLengths, paramFormats, - resultFormat); + paramForceColumnEncryptions, + resultFormat, + paramDesc); } /* @@ -1812,7 +2260,9 @@ PQsendQueryGuts(PGconn *conn, const char *const *paramValues, const int *paramLengths, const int *paramFormats, - int resultFormat) + const int *paramForceColumnEncryptions, + int resultFormat, + PGresult *paramDesc) { int i; PGcmdQueueEntry *entry; @@ -1859,14 +2309,53 @@ PQsendQueryGuts(PGconn *conn, pqPuts(stmtName, conn) < 0) goto sendFailed; + /* Check force column encryption */ + if (nParams > 0 && paramForceColumnEncryptions) + { + for (i = 0; i < nParams; i++) + { + if (paramForceColumnEncryptions[i]) + { + if (!(paramDesc && + paramDesc->paramDescs && + paramDesc->paramDescs[i].cekid)) + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("parameter with forced encryption is not to be encrypted\n")); + goto sendFailed; + } + } + } + } + /* Send parameter formats */ - if (nParams > 0 && paramFormats) + if (nParams > 0 && (paramFormats || (paramDesc && paramDesc->paramDescs))) { if (pqPutInt(nParams, 2, conn) < 0) goto sendFailed; + for (i = 0; i < nParams; i++) { - if (pqPutInt(paramFormats[i], 2, conn) < 0) + int format = paramFormats ? paramFormats[i] : 0; + + if (paramDesc && paramDesc->paramDescs) + { + PGresParamDesc *pd = ¶mDesc->paramDescs[i]; + + if (pd->cekid) + { + if (format != 0) + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("format must be text for encrypted parameter\n")); + goto sendFailed; + } + format = 1; + format |= 0x10; + } + } + + if (pqPutInt(format, 2, conn) < 0) goto sendFailed; } } @@ -1885,6 +2374,7 @@ PQsendQueryGuts(PGconn *conn, if (paramValues && paramValues[i]) { int nbytes; + const char *paramValue; if (paramFormats && paramFormats[i] != 0) { @@ -1903,9 +2393,54 @@ PQsendQueryGuts(PGconn *conn, /* text parameter, do not use paramLengths */ nbytes = strlen(paramValues[i]); } + + paramValue = paramValues[i]; + + if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid) + { +#ifdef USE_SSL + bool enc_det = (paramDesc->paramDescs[i].flags & 0x01) != 0; + PGCEK *cek = NULL; + char *enc_paramValue; + int enc_nbytes = nbytes; + + for (int j = 0; j < conn->nceks; j++) + { + if (conn->ceks[j].cekid == paramDesc->paramDescs[i].cekid) + { + cek = &conn->ceks[j]; + break; + } + } + if (!cek) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("column encryption key not found\n")); + goto sendFailed; + } + + enc_paramValue = (char *) encrypt_value(conn, cek, paramDesc->paramDescs[i].cekalg, + (const unsigned char *) paramValue, &enc_nbytes, enc_det); + if (!enc_paramValue) + goto sendFailed; + + if (pqPutInt(enc_nbytes, 4, conn) < 0 || + pqPutnchar(enc_paramValue, enc_nbytes, conn) < 0) + goto sendFailed; + + free(enc_paramValue); +#else + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("column encryption not supported by this build\n")); + goto sendFailed; +#endif + } + else + { if (pqPutInt(nbytes, 4, conn) < 0 || - pqPutnchar(paramValues[i], nbytes, conn) < 0) + pqPutnchar(paramValue, nbytes, conn) < 0) goto sendFailed; + } } else { @@ -2338,12 +2873,27 @@ PQexecPrepared(PGconn *conn, const int *paramLengths, const int *paramFormats, int resultFormat) +{ + return PQexecPrepared2(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, NULL, resultFormat, NULL); +} + +PGresult * +PQexecPrepared2(PGconn *conn, + const char *stmtName, + int nParams, + const char *const *paramValues, + const int *paramLengths, + const int *paramFormats, + const int *paramForceColumnEncryptions, + int resultFormat, + PGresult *paramDesc) { if (!PQexecStart(conn)) return NULL; - if (!PQsendQueryPrepared(conn, stmtName, - nParams, paramValues, paramLengths, - paramFormats, resultFormat)) + if (!PQsendQueryPrepared2(conn, stmtName, + nParams, paramValues, paramLengths, + paramFormats, paramForceColumnEncryptions, + resultFormat, paramDesc)) return NULL; return PQexecFinish(conn); } @@ -3607,6 +4157,17 @@ PQfmod(const PGresult *res, int field_num) return 0; } +int +PQfisencrypted(const PGresult *res, int field_num) +{ + if (!check_field_number(res, field_num)) + return false; + if (res->attDescs) + return (res->attDescs[field_num].cekid != 0); + else + return false; +} + char * PQcmdStatus(PGresult *res) { @@ -3792,6 +4353,17 @@ PQparamtype(const PGresult *res, int param_num) return InvalidOid; } +int +PQparamisencrypted(const PGresult *res, int param_num) +{ + if (!check_param_number(res, param_num)) + return false; + if (res->paramDescs) + return (res->paramDescs[param_num].cekid != 0); + else + return false; +} + /* PQsetnonblocking: * sets the PGconn's database connection non-blocking if the arg is true diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c index 10c76daf6e..9461c04bbe 100644 --- a/src/interfaces/libpq/fe-protocol3.c +++ b/src/interfaces/libpq/fe-protocol3.c @@ -45,6 +45,8 @@ static int getRowDescriptions(PGconn *conn, int msgLength); static int getParamDescriptions(PGconn *conn, int msgLength); static int getAnotherTuple(PGconn *conn, int msgLength); static int getParameterStatus(PGconn *conn); +static int getColumnMasterKey(PGconn *conn); +static int getColumnEncryptionKey(PGconn *conn); static int getNotify(PGconn *conn); static int getCopyStart(PGconn *conn, ExecStatusType copytype); static int getReadyForQuery(PGconn *conn); @@ -315,6 +317,22 @@ pqParseInput3(PGconn *conn) if (pqGetInt(&(conn->be_key), 4, conn)) return; break; + case 'y': /* Column Master Key */ + if (getColumnMasterKey(conn)) + { + // TODO: review error handling here + pqSaveErrorResult(conn); + pqInternalNotice(&conn->noticeHooks, "%s", conn->errorMessage.data); + } + break; + case 'Y': /* Column Encryption Key */ + if (getColumnEncryptionKey(conn)) + { + // TODO: review error handling here + pqSaveErrorResult(conn); + pqInternalNotice(&conn->noticeHooks, "%s", conn->errorMessage.data); + } + break; case 'T': /* Row Description */ if (conn->error_result || (conn->result != NULL && @@ -572,6 +590,8 @@ getRowDescriptions(PGconn *conn, int msgLength) int typlen; int atttypmod; int format; + int cekid; + int cekalg; if (pqGets(&conn->workBuffer, conn) || pqGetInt(&tableid, 4, conn) || @@ -579,7 +599,9 @@ getRowDescriptions(PGconn *conn, int msgLength) pqGetInt(&typid, 4, conn) || pqGetInt(&typlen, 2, conn) || pqGetInt(&atttypmod, 4, conn) || - pqGetInt(&format, 2, conn)) + pqGetInt(&format, 2, conn) || + pqGetInt(&cekid, 4, conn) || + pqGetInt(&cekalg, 2, conn)) { /* We should not run out of data here, so complain */ errmsg = libpq_gettext("insufficient data in \"T\" message"); @@ -607,8 +629,10 @@ getRowDescriptions(PGconn *conn, int msgLength) result->attDescs[i].typid = typid; result->attDescs[i].typlen = typlen; result->attDescs[i].atttypmod = atttypmod; + result->attDescs[i].cekid = cekid; + result->attDescs[i].cekalg = cekalg; - if (format != 1) + if ((format & 0x0F) != 1) result->binary = 0; } @@ -710,10 +734,22 @@ getParamDescriptions(PGconn *conn, int msgLength) for (i = 0; i < nparams; i++) { int typid; + int cekid; + int cekalg; + int flags; if (pqGetInt(&typid, 4, conn)) goto not_enough_data; result->paramDescs[i].typid = typid; + if (pqGetInt(&cekid, 4, conn)) + goto not_enough_data; + result->paramDescs[i].cekid = cekid; + if (pqGetInt(&cekalg, 2, conn)) + goto not_enough_data; + result->paramDescs[i].cekalg = cekalg; + if (pqGetInt(&flags, 2, conn)) + goto not_enough_data; + result->paramDescs[i].flags = flags; } /* Success! */ @@ -1439,6 +1475,89 @@ getParameterStatus(PGconn *conn) return 0; } +/* + * Attempt to read a ColumnMasterKey message. + * Entry: 'y' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message. + * returns EOF if not enough data. + */ +static int +getColumnMasterKey(PGconn *conn) +{ + int keyid; + char *keyname; + char *keyrealm; + int ret; + + /* Get the key ID */ + if (pqGetInt(&keyid, 4, conn) != 0) + return EOF; + /* Get the key name */ + if (pqGets(&conn->workBuffer, conn) != 0) + return EOF; + keyname = strdup(conn->workBuffer.data); + if (!keyname) + return EOF; + /* Get the key realm */ + if (pqGets(&conn->workBuffer, conn) != 0) + return EOF; + keyrealm = strdup(conn->workBuffer.data); + if (!keyrealm) + return EOF; + /* And save it */ + ret = pqSaveColumnMasterKey(conn, keyid, keyname, keyrealm); + + free(keyname); + free(keyrealm); + + return ret; +} + +/* + * Attempt to read a ColumnEncryptionKey message. + * Entry: 'Y' message type and length have already been consumed. + * Exit: returns 0 if successfully consumed message. + * returns EOF if not enough data. + */ +static int +getColumnEncryptionKey(PGconn *conn) +{ + int keyid; + int cmkid; + int cmkalg; + char *buf; + int vallen; + + /* Get the key ID */ + if (pqGetInt(&keyid, 4, conn) != 0) + return EOF; + /* Get the CMK ID */ + if (pqGetInt(&cmkid, 4, conn) != 0) + return EOF; + /* Get the CMK algorithm */ + if (pqGetInt(&cmkalg, 2, conn) != 0) + return EOF; + /* Get the key data len */ + if (pqGetInt(&vallen, 4, conn) != 0) + return EOF; + /* Get the key data */ + buf = malloc(vallen); + if (!buf) + return EOF; + if (pqGetnchar(buf, vallen, conn) != 0) + { + free(buf); + return EOF; + } + /* And save it */ + if (pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen) != 0) + { + free(buf); + return EOF; + } + free(buf); + return 0; +} /* * Attempt to read a Notify response message. diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c index 5d68cf2eb3..e08cbcff33 100644 --- a/src/interfaces/libpq/fe-trace.c +++ b/src/interfaces/libpq/fe-trace.c @@ -458,7 +458,12 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress) nfields = pqTraceOutputInt16(f, message, cursor); for (int i = 0; i < nfields; i++) + { + pqTraceOutputInt32(f, message, cursor, regress); pqTraceOutputInt32(f, message, cursor, regress); + pqTraceOutputInt16(f, message, cursor); + pqTraceOutputInt16(f, message, cursor); + } } /* RowDescription */ @@ -479,6 +484,8 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress) pqTraceOutputInt16(f, message, cursor); pqTraceOutputInt32(f, message, cursor, false); pqTraceOutputInt16(f, message, cursor); + pqTraceOutputInt32(f, message, cursor, regress); + pqTraceOutputInt16(f, message, cursor); } } diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 7986445f1a..42fbe0cd49 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -267,6 +267,8 @@ typedef struct pgresAttDesc Oid typid; /* type id */ int typlen; /* type size */ int atttypmod; /* type-specific modifier info */ + Oid cekid; + int cekalg; } PGresAttDesc; /* ---------------- @@ -438,6 +440,15 @@ extern PGresult *PQexecPrepared(PGconn *conn, const int *paramLengths, const int *paramFormats, int resultFormat); +extern PGresult *PQexecPrepared2(PGconn *conn, + const char *stmtName, + int nParams, + const char *const *paramValues, + const int *paramLengths, + const int *paramFormats, + const int *paramForceColumnEncryptions, + int resultFormat, + PGresult *paramDesc); /* Interface for multiple-result or asynchronous queries */ #define PQ_QUERY_PARAM_MAX_LIMIT 65535 @@ -461,6 +472,15 @@ extern int PQsendQueryPrepared(PGconn *conn, const int *paramLengths, const int *paramFormats, int resultFormat); +extern int PQsendQueryPrepared2(PGconn *conn, + const char *stmtName, + int nParams, + const char *const *paramValues, + const int *paramLengths, + const int *paramFormats, + const int *paramForceColumnEncryptions, + int resultFormat, + PGresult *paramDesc); extern int PQsetSingleRowMode(PGconn *conn); extern PGresult *PQgetResult(PGconn *conn); @@ -531,6 +551,7 @@ extern int PQfformat(const PGresult *res, int field_num); extern Oid PQftype(const PGresult *res, int field_num); extern int PQfsize(const PGresult *res, int field_num); extern int PQfmod(const PGresult *res, int field_num); +extern int PQfisencrypted(const PGresult *res, int field_num); extern char *PQcmdStatus(PGresult *res); extern char *PQoidStatus(const PGresult *res); /* old and ugly */ extern Oid PQoidValue(const PGresult *res); /* new and improved */ @@ -540,6 +561,7 @@ extern int PQgetlength(const PGresult *res, int tup_num, int field_num); extern int PQgetisnull(const PGresult *res, int tup_num, int field_num); extern int PQnparams(const PGresult *res); extern Oid PQparamtype(const PGresult *res, int param_num); +extern int PQparamisencrypted(const PGresult *res, int param_num); /* Describe prepared statements and portals */ extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 3db6a17db4..0a4c012aa6 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -111,6 +111,9 @@ union pgresult_data typedef struct pgresParamDesc { Oid typid; /* type id */ + Oid cekid; + int cekalg; + int flags; } PGresParamDesc; /* @@ -340,6 +343,26 @@ typedef struct pg_conn_host * found in password file. */ } pg_conn_host; +/* + * Column encryption support data + */ + +/* column master key */ +typedef struct pg_cmk +{ + Oid cmkid; + char *cmkname; + char *cmkrealm; +} PGCMK; + +/* column encryption key */ +typedef struct pg_cek +{ + Oid cekid; + unsigned char *cekdata; /* (decrypted) */ + size_t cekdatalen; +} PGCEK; + /* * PGconn stores all the state data associated with a single connection * to a backend. @@ -393,6 +416,7 @@ struct pg_conn char *ssl_min_protocol_version; /* minimum TLS protocol version */ char *ssl_max_protocol_version; /* maximum TLS protocol version */ char *target_session_attrs; /* desired session properties */ + char *cmklookup; /* CMK lookup specification */ /* Optional file to write trace info to */ FILE *Pfdebug; @@ -475,6 +499,12 @@ struct pg_conn PGContextVisibility show_context; /* whether to show CONTEXT field */ PGlobjfuncs *lobjfuncs; /* private state for large-object access fns */ + /* Column encryption support data */ + int ncmks; + PGCMK *cmks; + int nceks; + PGCEK *ceks; + /* Buffer for data received from backend and not yet processed */ char *inBuffer; /* currently allocated buffer */ int inBufSize; /* allocated size of buffer */ @@ -670,6 +700,10 @@ extern void pqSaveMessageField(PGresult *res, char code, const char *value); extern void pqSaveParameterStatus(PGconn *conn, const char *name, const char *value); +extern int pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname, + const char *keyrealm); +extern int pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, + const unsigned char *value, int len); extern int pqRowProcessor(PGconn *conn, const char **errmsgp); extern void pqCommandQueueAdvance(PGconn *conn); extern int PQsendQueryContinue(PGconn *conn, const char *query); diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk index 1f62ba1b57..844e085e6d 100644 --- a/src/interfaces/libpq/nls.mk +++ b/src/interfaces/libpq/nls.mk @@ -1,6 +1,6 @@ # src/interfaces/libpq/nls.mk CATALOG_NAME = libpq AVAIL_LANGUAGES = cs de el es fr he it ja ko pl pt_BR ru sv tr uk zh_CN zh_TW -GETTEXT_FILES = fe-auth.c fe-auth-scram.c fe-connect.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c +GETTEXT_FILES = fe-auth.c fe-auth-scram.c fe-connect.c fe-encrypt-openssl.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c GETTEXT_TRIGGERS = libpq_gettext pqInternalNotice:2 GETTEXT_FLAGS = libpq_gettext:1:pass-c-format pqInternalNotice:2:c-format diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore index 6ba78adb67..1846594ec5 100644 --- a/src/interfaces/libpq/test/.gitignore +++ b/src/interfaces/libpq/test/.gitignore @@ -1,2 +1,3 @@ +/libpq_test_encrypt /libpq_testclient /libpq_uri_regress diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile index 1f75b73b8c..cf546a6254 100644 --- a/src/interfaces/libpq/test/Makefile +++ b/src/interfaces/libpq/test/Makefile @@ -12,8 +12,15 @@ override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS) LDFLAGS_INTERNAL += $(libpq_pgport) PROGS = libpq_testclient libpq_uri_regress +ifeq ($(with_ssl),openssl) +PROGS += libpq_test_encrypt +endif + all: $(PROGS) +libpq_test_encrypt: ../fe-encrypt-openssl.c + $(CC) $(CPPFLAGS) -DTEST_ENCRYPT $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X) + clean distclean maintainer-clean: rm -f $(PROGS) *.o diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 44457f930c..c5f3153094 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -9529,7 +9529,7 @@ DO $d$ END; $d$; ERROR: invalid option "password" -HINT: Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections +HINT: Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, cmklookup, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections CONTEXT: SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')" PL/pgSQL function inline_code_block line 3 at EXECUTE -- If we add a password for our user mapping instead, we should get a different base-commit: c1d033fcb5ecf306241cd729d1edcaa846456335 -- 2.36.1