public inbox for [email protected]  
help / color / mirror / Atom feed
From: Peter Eisentraut <[email protected]>
To: pgsql-hackers <[email protected]>
Subject: Re: Transparent column encryption
Date: Tue, 27 Sep 2022 15:51:25 +0200
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>
References: <[email protected]>
	<[email protected]>
	<[email protected]>
	<[email protected]>
	<[email protected]>
	<[email protected]>
	<[email protected]>
	<[email protected]>

Updated version with meson build system support added (for added files 
and new tests).

On 21.09.22 23:37, Peter Eisentraut wrote:
> New version with some merge conflicts resolved, and I have worked to 
> resolve several "TODO" items that I had noted in the code.
> 
> On 13.09.22 10:27, Peter Eisentraut wrote:
>> Here is an updated patch that resolves some merge conflicts; no 
>> functionality changes over v6.
>>
>> On 30.08.22 13:35, Peter Eisentraut wrote:
>>> Here is an updated patch.
>>>
>>> I mainly spent time on adding a full set of DDL commands for the 
>>> keys. This made the patch very bulky now, but there is not really 
>>> anything surprising in there.  It probably needs another check of 
>>> permission handling etc., but it's got everything there to try it 
>>> out.  Along with the DDL commands, the pg_dump side is now fully 
>>> implemented.
>>>
>>> Secondly, I isolated the protocol changes into a protocol extension 
>>> with the name _pq_.column_encryption.  So by default there are no 
>>> protocol changes and this feature is disabled.  AFAICT, we haven't 
>>> actually ever used the _pq_ protocol extension mechanism, so it would 
>>> be good to review whether this was done here in the intended way.
>>>
>>> At this point, the patch is sort of feature complete, meaning it has 
>>> all the concepts, commands, and interfaces that I had in mind.  I 
>>> have a long list of things to recheck and tighten up, based on 
>>> earlier feedback and some things I found along the way.  But I don't 
>>> currently plan any more major architectural or design changes, 
>>> pending feedback.  (Also, the patch is now very big, so anything 
>>> additional might be better for a future separate patch.)

From 6a055b8d12d26c3728f517432cee5424628f02da Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Tue, 27 Sep 2022 15:48:44 +0200
Subject: [PATCH v9] Transparent column encryption

This feature enables the automatic, transparent encryption and
decryption of particular columns in the client.  The data for those
columns then only ever appears in ciphertext on the server, so it is
protected from DBAs, sysadmins, cloud operators, etc. as well as
accidental leakage to server logs, file-system backups, etc.  The
canonical use case for this feature is storing credit card numbers
encrypted, in accordance with PCI DSS, as well as similar situations
involving social security numbers etc.  One can't do any computations
with encrypted values on the server, but for these use cases, that is
not necessary.  This feature does support deterministic encryption as
an alternative to the default randomized encryption, so in that mode
one can do equality lookups, at the cost of some security.

This functionality also exists in other database products, and the
overall concepts were mostly adopted from there.

(Note: This feature has nothing to do with any on-disk encryption
feature.  Both can exist independently.)

You declare a column as encrypted in a CREATE TABLE statement.  The
column value is encrypted by a symmetric key called the column
encryption key (CEK).  The CEK is a catalog object.  The CEK key
material is in turn encrypted by an assymmetric key called the column
master key (CMK).  The CMK is not stored in the database but somewhere
where the client can get to it, for example in a file or in a key
management system.  When a server sends rows containing encrypted
column values to the client, it first sends the required CMK and CEK
information (new protocol messages), which the client needs to record.
Then, the client can use this information to automatically decrypt the
incoming row data and forward it in plaintext to the application.

For the CMKs, libpq has a new connection parameter "cmklookup" that
specifies via a mini-language where to get the keys.  Right now, you
can use "file" to read it from a file, or "run" to run some program,
which could get it from a KMS.

The general idea would be for an application to have one CMK per area
of secret stuff, for example, for credit card data.  The CMK can be
rotated: each CEK can be represented multiple times in the database,
encrypted by a different CMK.  (The CEK can't be rotated easily, since
that would require reading out all the data from a table/column and
reencrypting it.  We could/should add some custom tooling for that,
but it wouldn't be a routine operation.)

Several encryption algorithms are provided.  The CMK process uses
PG_CMK_RSAES_OAEP_SHA_1 or _256.  The CEK process uses
AEAD_AES_*_CBC_HMAC_SHA_* with several strengths.

In the server, the encrypted datums are stored in types called
pg_encrypted_rnd and pg_encrypted_det (for randomized and
deterministic encryption).  These are essentially cousins of bytea.
For the rest of the database system below the protocol handling, there
is nothing special about those.  For example, pg_encrypted_rnd has no
operators at all, pg_encrypted_det has only an equality operator.
pg_attribute has a new column attrealtypid that stores the original
type of the data in the column.  This is only used for providing it to
clients, so that higher-level clients can convert the decrypted value
to their appropriate data types in their environments.

The protocol extensions are guarded by a new protocol extension option
"_pq_.column_encryption".  If this is not set, nothing changes, the
protocol stays the same, and no encryption or decryption happens.

The trickiest part of this whole thing appears to be how to get
transparently encrypted data into the database (as opposed to reading
it out).  It is required to use protocol-level prepared statements
(i.e., extended query) for this.  The client must first prepare a
statement, then describe the statement to get parameter metadata,
which indicates which parameters are to be encrypted and how.  So this
will require some care by applications that want to do this, but,
well, they probably should be careful anyway.  In libpq, the existing
APIs make this difficult, because there is no way to pass the result
of a describe-statement call back into
execute-statement-with-parameters.  I added new functions that do
this, so you then essentially do

    res0 = PQdescribePrepared(conn, "");
    res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, 0, res0);

(The name could obviously be improved.)  Other client APIs that have a
"statement handle" concept could do this more elegantly and probably
without any API changes.

Another challenge is that the parse analysis must check which
underlying column a parameter corresponds to.  This is similar to
resorigtbl and resorigcol in the opposite direction.  The current
implementation of this works for the test cases, but I know it has
some problems, so I'll continue working in this.  This functionality
is in principle available to all prepared-statement variants, not only
protocol-level.  So you can see in the tests that I expanded the
pg_prepared_statements view to show this information as well, which
also provides an easy way to test and debug this functionality
independent of column encryption.

psql doesn't use prepared statements, so writing into encrypted
columns wouldn't work at all via psql.  (Reading works no
problem.)  I added a new psql command \gencr that you can use like

INSERT INTO t1 VALUES ($1, $2) \gencr 'val1' 'val2'

TODO: This patch version has all the necessary pieces in place to make
this work, so you can have an idea how the overall system works.  It
contains some documentation and tests to help illustrate the
functionality.  Missing:

- psql \d support

Discussion: https://www.postgresql.org/message-id/flat/89157929-c2b6-817b-6025-8e4b2d89d88f%40enterprisedb.com
---
 doc/src/sgml/acronyms.sgml                    |  18 +
 doc/src/sgml/catalogs.sgml                    | 275 +++++++
 doc/src/sgml/datatype.sgml                    |  47 ++
 doc/src/sgml/ddl.sgml                         | 379 +++++++++
 doc/src/sgml/glossary.sgml                    |  23 +
 doc/src/sgml/libpq.sgml                       | 280 ++++++-
 doc/src/sgml/protocol.sgml                    | 392 +++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 186 +++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 117 +++
 .../ref/create_column_encryption_key.sgml     | 163 ++++
 .../sgml/ref/create_column_master_key.sgml    | 106 +++
 doc/src/sgml/ref/create_table.sgml            |  42 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/pg_dump.sgml                 |  31 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  33 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   7 +
 src/backend/access/common/printtup.c          | 191 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  64 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |   3 +
 src/backend/catalog/objectaddress.c           | 220 +++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  15 +
 src/backend/commands/colenccmds.c             | 354 ++++++++
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/prepare.c                |  74 +-
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              |  96 +++
 src/backend/commands/variable.c               |   7 +-
 src/backend/executor/spi.c                    |   4 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/analyze.c                  |   3 +-
 src/backend/parser/gram.y                     | 121 ++-
 src/backend/parser/parse_param.c              |  49 +-
 src/backend/parser/parse_target.c             |   6 +
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  57 +-
 src/backend/tcop/utility.c                    |  42 +-
 src/backend/utils/adt/arrayfuncs.c            |   1 +
 src/backend/utils/cache/lsyscache.c           | 111 +++
 src/backend/utils/cache/plancache.c           |  31 +-
 src/backend/utils/cache/syscache.c            |  58 ++
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |   6 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   4 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 350 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  31 +
 src/bin/pg_dump/pg_dump_sort.c                |  14 +
 src/bin/pg_dump/pg_dumpall.c                  |   5 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  56 +-
 src/bin/psql/command.c                        |  31 +
 src/bin/psql/common.c                         |  39 +-
 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                   |  51 +-
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/meson.build               |   3 +
 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/commands/colenccmds.h             |  25 +
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  17 +
 src/include/parser/analyze.h                  |   4 +-
 src/include/parser/kwlist.h                   |   2 +
 src/include/parser/parse_node.h               |   2 +
 src/include/parser/parse_param.h              |   3 +-
 src/include/tcop/cmdtaglist.h                 |   6 +
 src/include/tcop/tcopprot.h                   |   2 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   6 +
 src/include/utils/plancache.h                 |   6 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  38 +-
 src/interfaces/libpq/fe-encrypt-openssl.c     | 766 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 593 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 180 +++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  22 +
 src/interfaces/libpq/libpq-int.h              |  37 +
 src/interfaces/libpq/meson.build              |   1 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |   2 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  29 +
 src/test/column_encryption/meson.build        |  20 +
 .../t/001_column_encryption.pl                | 183 +++++
 .../column_encryption/t/002_cmk_rotation.pl   | 108 +++
 src/test/column_encryption/test_client.c      | 210 +++++
 .../column_encryption/test_run_decrypt.pl     |  41 +
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 143 ++++
 src/test/regress/expected/object_address.out  | 153 ++--
 src/test/regress/expected/oidjoins.out        |   6 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/prepare.out         |  38 +-
 src/test/regress/expected/psql.out            |  25 +
 src/test/regress/expected/rules.out           |   4 +-
 src/test/regress/expected/type_sanity.out     |  46 +-
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 124 +++
 src/test/regress/sql/object_address.sql       |  18 +-
 src/test/regress/sql/prepare.sql              |   2 +-
 src/test/regress/sql/psql.sql                 |  17 +
 src/test/regress/sql/type_sanity.sql          |   2 +
 135 files changed, 7633 insertions(+), 208 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/alter_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_master_key.sgml
 create mode 100644 src/backend/commands/colenccmds.c
 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/include/commands/colenccmds.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/meson.build
 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

diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 2df6559accce..bd1a0185ed7a 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -56,6 +56,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CEK</acronym></term>
+    <listitem>
+     <para>
+      Column Encryption Key
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CIDR</acronym></term>
     <listitem>
@@ -67,6 +76,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CMK</acronym></term>
+    <listitem>
+     <para>
+      Column Master Key
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CPAN</acronym></term>
     <listitem>
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 00f833d210e7..638be1c8507b 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -105,6 +105,21 @@ <title>System Catalogs</title>
       <entry>collations (locale information)</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link></entry>
+      <entry>column encryption keys</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link></entry>
+      <entry>column encryption key data</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link></entry>
+      <entry>column master keys</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link></entry>
       <entry>check constraints, unique constraints, primary key constraints, foreign key constraints</entry>
@@ -1360,6 +1375,40 @@ <title><structname>pg_attribute</structname> Columns</title>
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attcek</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, a reference to the column encryption key, else 0.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attrealtypid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       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 <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.  If the
+       column is not encrypted, then 0.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attencalg</structfield> <type>int16</type>
+      </para>
+      <para>
+       If the column is encrypted, the identifier of the encryption algorithm;
+       see <xref linkend="protocol-cek"/> for possible values.
+       </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>attinhcount</structfield> <type>int4</type>
@@ -2456,6 +2505,232 @@ <title><structname>pg_collation</structname> Columns</title>
   </para>
  </sect1>
 
+ <sect1 id="catalog-pg-colenckey">
+  <title><structname>pg_colenckey</structname></title>
+
+  <indexterm zone="catalog-pg-colenckey">
+   <primary>pg_colenckey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckey</structname> contains information
+   about the column encryption keys in the database.  The actual key material
+   of the column encryption keys is in the catalog <link
+   linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link>.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column encryption key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column encryption key
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colenckeydata">
+  <title><structname>pg_colenckeydata</structname></title>
+
+  <indexterm zone="catalog-pg-colenckeydata">
+   <primary>pg_colenckeydata</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckeydata</structname> 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, <literal>(ckdcekid, ckdcmkid)</literal> is a
+   unique key of this table.
+  </para>
+
+  <para>
+   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.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckeydata</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcekid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column encryption key this entry belongs to
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column master key that the key material is encrypted with
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkalg</structfield> <type>int16</type>
+      </para>
+      <para>
+       The encryption algorithm used for encrypting the key material; see
+       <xref linkend="protocol-cmk"/> for possible values.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdencval</structfield> <type>bytea</type>
+      </para>
+      <para>
+       The key material of this column encryption key, encrypted using the
+       referenced column master key
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colmasterkey">
+  <title><structname>pg_colmasterkey</structname></title>
+
+  <indexterm zone="catalog-pg-colmasterkey">
+   <primary>pg_colmasterkey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colmasterkey</structname> 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.
+  </para>
+
+  <table>
+   <title><structname>pg_colmasterkey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column master key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkrealm</structfield> <type>text</type>
+      </para>
+      <para>
+       A <quote>realm</quote> 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.
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 0258b192e0e4..05286dd3e530 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5346,4 +5346,51 @@ <title>Pseudo-Types</title>
 
   </sect1>
 
+  <sect1 id="datatype-encrypted">
+   <title>Types Related to Encryption</title>
+
+   <para>
+    An encrypted column value (see <xref linkend="ddl-column-encryption"/>) is
+    internally stored using the types
+    <type>pg_encrypted_rnd</type> (for randomized encryption) or
+    <type>pg_encrypted_det</type> (for deterministic encryption); see <xref
+    linkend="datatype-encrypted-table"/>.  Most of the database system treats
+    this as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  The external
+    representation of these types is the same as the <type>bytea</type> 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.
+   </para>
+
+    <table id="datatype-encrypted-table">
+     <title>Types Related to Encryption</title>
+     <tgroup cols="3">
+      <colspec colname="col1" colwidth="1*"/>
+      <colspec colname="col2" colwidth="3*"/>
+      <colspec colname="col3" colwidth="2*"/>
+      <thead>
+       <row>
+        <entry>Name</entry>
+        <entry>Storage Size</entry>
+        <entry>Description</entry>
+       </row>
+      </thead>
+      <tbody>
+       <row>
+        <entry><type>pg_encrypted_det</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, deterministic encryption</entry>
+       </row>
+       <row>
+        <entry><type>pg_encrypted_rnd</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, randomized encryption</entry>
+       </row>
+      </tbody>
+     </tgroup>
+    </table>
+  </sect1>
  </chapter>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 03c01937094b..8cf69fe10fef 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1211,6 +1211,385 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Transparent Column Encryption</title>
+
+  <para>
+   With <firstterm>transparent column encryption</firstterm>, 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.
+  </para>
+
+  <sect2>
+   <title>Using Transparent Column Encryption</title>
+
+  <para>
+   Tranparent column encryption uses two levels of cryptographic keys.  The
+   actual column value is encrypted using a symmetric algorithm, such as AES,
+   using a <firstterm>column encryption key</firstterm>
+   (<acronym>CEK</acronym>).  The column encryption key is in turn encrypted
+   using an assymmetric algorithm, such as RSA, using a <firstterm>column
+   master key</firstterm> (<acronym>CMK</acronym>).  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.
+  </para>
+
+  <para>
+   Here is an example declaring a column as encrypted:
+<programlisting>
+CREATE TABLE customers (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    creditcard_num text <emphasis>ENCRYPTED WITH (column_encryption_key = cek1)</emphasis>
+);
+</programlisting>
+  </para>
+
+  <para>
+   Column encryption supports <firstterm>randomized</firstterm>
+   (also known as <firstterm>probabilistic</firstterm>) and
+   <firstterm>deterministic</firstterm> 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:
+<programlisting>
+CREATE TABLE employees (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    ssn text ENCRYPTED WITH (
+        column_encryption_key = cek1, <emphasis>encryption_type = deterministic</emphasis>)
+);
+</programlisting>
+  </para>
+
+  <para>
+   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.
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Reading and Writing Encrypted Columns</title>
+
+   <para>
+    Reading and writing encrypted columns is meant to be handled automatically
+    by the client library/driver and should be mostly transparent to the
+    application code, if certain prerequisites are fulfilled:
+
+    <itemizedlist>
+     <listitem>
+      <para>
+       The client library needs to support transparent column encryption.  Not
+       all client libraries do.  Furthermore, the client library might require
+       that transparent column encryption is explicitly enabled at connection
+       time.  See the documentation of the client library for details.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Column master keys and column encryption keys have been set up, and the
+       client library has been configured to be able to look up column master
+       keys from the key store or key management system.
+      </para>
+     </listitem>
+    </itemizedlist>
+   </para>
+
+   <para>
+    Reading from encrypted columns will then work automatically.  For example,
+    using the above example,
+<programlisting>
+SELECT ssn FROM employees WHERE id = 5;
+</programlisting>
+    will return the unencrypted value for the <literal>ssn</literal> column in
+    any rows found.
+   </para>
+
+   <para>
+    Writing to encrypted columns requires that the extended query protocol
+    (protocol-level prepared statements) be used, so that the values to be
+    encrypted are supplied separately from the SQL command.  For example,
+    using, say, psql or libpq, the following would not work:
+<programlisting>
+-- WRONG!
+INSERT INTO ssn (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This will leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of column encryption.
+    Note that using server-side prepared statements using the SQL commands
+    <command>PREPARE</command> and <command>EXECUTE</command> is equally
+    incorrect, since that would also leak the parameters provided to
+    <command>EXECUTE</command> to the server.
+   </para>
+
+   <para>
+    The correct notional order of operations is illustrated here using
+    libpq-like code (without error checking):
+<programlisting>
+PGresult   *tmpres,
+           *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+PQprepare(conn, "", "INSERT INTO ssn (id, name, ssn) VALUES ($1, $2, $3)", 3, NULL);
+
+/* This fetches information about which parameters need to be encrypted. */
+tmpres = PQdescribePrepared(conn, "");
+
+res = PQexecPrepared2(conn, "", 3, values, NULL, NULL, NULL, 0, tmpres);
+
+/* print result in res */
+</programlisting>
+    Higher-level client libraries might use the protocol-level prepared
+    statements automatically and thus won't require any code changes.
+   </para>
+
+   <para>
+    <application>psql</application> provides the command
+    <literal>\gencr</literal> to run prepared statements like this:
+<programlisting>
+INSERT INTO ssn (id, name, ssn) VALUES ($1, $2, $3) \gencr '1' 'Someone', '12345'
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Transparent Column Encryption</title>
+
+  <para>
+   The steps to set up transparent column encryption for a database are:
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK, for example, using a cryptographic
+      library or toolkit, or a key management system.  Secure access to the
+      key as appropriate, using access control, passwords, etc.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database using the SQL command <xref linkend="sql-create-column-master-key"/>.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the (unencrypted) key material for the CEK in a temporary
+      location.  (It will be encrypted in the next step.  Depending on the
+      available tools, it might be possible and sensible to combine these two
+      steps.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material using the CMK (created earlier).
+      (The unencrypted version of the CEK key material can now be disposed
+      of.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database using the SQL command <xref
+      linkend="sql-create-column-encryption-key"/>.  This command
+      <quote>uploads</quote> the encrypted CEK key material created in the
+      previous step to the database server.  The local copy of the CEK key
+      material can then be removed.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns using the created CEK.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the client library/driver to be able to look up the CMK
+      created earlier.
+     </para>
+    </listitem>
+   </orderedlist>
+
+   Once this is done, values can be written to and read from the encrypted
+   columns in a transparent way.
+  </para>
+
+  <para>
+   Note that these steps should not be run on the database server, but on some
+   client machine.  Neither the CMK nor the unencrypted CEK should ever appear
+   on the database server host.
+  </para>
+
+  <para>
+   The specific details of this setup depend on the desired CMK storage
+   mechanism/key management system as well as the client libraries to be used.
+   The following example uses the <command>openssl</command> command-line tool
+   to set up the keys.
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK and write it to a file:
+<programlisting>
+openssl genpkey -algorithm rsa -out cmk1.pem
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database:
+<programlisting>
+psql ... -c "CREATE COLUMN MASTER KEY cmk1 WITH (realm = '')"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 32
+</programlisting>
+      (See <xref linkend="protocol-cek-table"/> for required key lengths.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material:
+<programlisting>
+openssl pkeyutl -encrypt -inkey cmk1.pem -pkeyopt rsa_padding_mode:oaep -in cek1.bin -out cek1.bin.enc
+rm cek1.bin
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database:
+<programlisting>
+# convert file contents to hex encoding; this is just one possible way
+cekenchex=$(perl -0ne 'print unpack "H*"' cek1.bin.enc)
+psql ... -c "CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, encrypted_value = '\\x${cekenchex}')"
+rm cek1.bin.enc
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns as shown in the examples above.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the libpq for CMK lookup (see also <xref linkend="libpq-connect-cmklookup"/>):
+<programlisting>
+PGCMKLOOKUP="*=file:$PWD/%k.pem"
+export PGCMKLOOKUP
+</programlisting>
+     </para>
+
+     <para>
+      Additionally, libpq requires that the connection parameter <xref
+      linkend="libpq-connect-column-encryption"/> be set in order to activate
+      the transparent column encryption functionality.  This should be done in
+      the connection parameters of the application, but an environment
+      variable (<envar>PGCOLUMNENCRYPTION</envar>) is also available.
+     </para>
+    </listitem>
+   </orderedlist>
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Guidance on Using Transparent Column Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use transparent column encryption, and what precautions need to be
+    taken to maintain its security.
+   </para>
+
+   <para>
+    In general, column encryption is never a replacement for additional
+    security and encryption techniques such as transmission encryption
+    (SSL/TLS), storage encryption, strong access control, and password
+    security.  Column encryption only targets specific use cases and should be
+    used in conjunction with additional security measures.
+   </para>
+
+   <para>
+    A typical use case for column encryption is to encrypt specific values
+    with additional security requirements, for example credit card numbers.
+    This would allow you to store that security-sensitive data together with
+    the rest of your data (thus getting various benefits, such as referential
+    integrity, consistent backups), while giving access to that data only to
+    specific clients and preventing accidental leakage on the server side
+    (server logs, file system backups, etc.).
+   </para>
+
+   <para>
+    Column encryption cannot hide the existence or absence of data, it can
+    only disguise the particular data that is known to exist.  For example,
+    storing a cleartext person name and an encrypted credit card number
+    indicates that the person has a credit card.  That might not reveal too
+    much if the database is for an online store and there is other data nearby
+    that shows that the person has recently made purchases.  But in another
+    example, storing a cleartext person name and an encrypted diagnosis in a
+    medical database probably indicates that the person has a medical issue.
+    Depending on the circumstances, that might not by itself be sufficient
+    security.
+   </para>
+
+   <para>
+    Encryption cannot completely hide the length of values.  The encryption
+    methods will pad values to multiples of the underlying cipher's block size
+    (usually 16 bytes), so some length differences will be unified this way.
+    There is no concern if all values are of the same length (e.g., credit
+    card numbers).  But if there are signficant length differences between
+    valid values and that length information is security-sensitive, then
+    application-specific workarounds such as padding would need to be applied.
+    How to do that securely is beyond the scope of this manual.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so is usually
+     subject to government or industry regulations.  This section is not meant
+     to provide complete instructions on how to do this correctly.  Please
+     seek additional advice when engaging in such projects.
+    </para>
+   </note>
+  </sect2>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index d6d0a3a8140c..10507e43911e 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -353,6 +353,29 @@ <title>Glossary</title>
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-column-encryption-key">
+   <glossterm>Column encryption key</glossterm>
+   <glossdef>
+    <para>
+     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.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt column encryption keys.  (So the
+     column master key is a <firstterm>key encryption key</firstterm>.)
+     Column master keys are stored outside the database system, for example in
+     a key management system.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-commit">
    <glossterm>Commit</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 57bfc8fc714d..1076d4664079 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,123 @@ <title>Parameter Key Words</title>
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-connect-column-encryption" xreflabel="column_encryption">
+      <term><literal>column_encryption</literal></term>
+      <listitem>
+       <para>
+        If set to 1, this enables transparent column encryption for the
+        connection.  If encrypted columns are queried and this is not enabled,
+        the encrypted value is returned.  See <xref
+        linkend="ddl-column-encryption"/> for more information about this
+        feature.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-cmklookup" xreflabel="cmklookup">
+      <term><literal>cmklookup</literal></term>
+      <listitem>
+       <para>
+        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 <literal>key=value</literal> entries separated
+        by semicolons.  Each key is the name of a key realm, or
+        <literal>*</literal> to match all realms.  The value is a
+        <literal>scheme:data</literal> 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:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>%a</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name (see <xref linkend="protocol-cmk-table"/>)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%b</literal></term>
+          <listitem>
+           <para>
+            The encrypted CEK data in Base64 encoding (only for the <literal>run</literal> scheme)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%r</literal></term>
+          <listitem>
+           <para>
+            The realm name
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        Available schemes are:
+        <variablelist>
+         <varlistentry>
+          <term><literal>file</literal></term>
+          <listitem>
+           <para>
+            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.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>run</literal></term>
+          <listitem>
+           <para>
+            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.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        The default value is empty.
+       </para>
+
+       <para>
+        Example:
+<programlisting>
+cmklookup="r1=file:/some/where/secrets/%k.pem;*=file:/else/where/%r/%k.pem"
+</programlisting>
+        This specification says, for keys in realm <quote>r1</quote>, load
+        them from the specified file, replacing <literal>%k</literal> by the
+        key name.  For keys in other realms, load them from the file,
+        replacing realm and key names as specified.
+       </para>
+
+       <para>
+        An example for interacting with a (hypothetical) key management
+        system:
+<programlisting>
+cmklookup="*=run:acmekms decrypt --key %k --alg %a --blob '%b'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -3012,6 +3129,57 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPrepared2">
+      <term><function>PQexecPrepared2</function><indexterm><primary>PQexecPrepared2</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Sends a request to execute a prepared statement with given
+        parameters, and waits for the result, with support for encrypted columns.
+<synopsis>
+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);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPrepared2"/> is like <xref
+        linkend="libpq-PQexecPrepared"/> with additional support for encrypted
+        columns.  The parameter <parameter>paramDesc</parameter> must be a
+        result set obtained from <xref linkend="libpq-PQdescribePrepared"/> on
+        the same prepared statement.
+       </para>
+
+       <para>
+        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.
+       </para>
+
+       <para>
+        The parameter <parameter>paramForceColumnEncryptions</parameter>
+        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.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3862,6 +4030,28 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQfisencrypted">
+     <term><function>PQfisencrypted</function><indexterm><primary>PQfisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given column came from an encrypted
+       column.  Column numbers start at 0.
+<synopsis>
+int PQfisencrypted(const PGresult *res,
+                   int column_number);
+</synopsis>
+      </para>
+
+      <para>
+       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.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQfsize">
      <term><function>PQfsize</function><indexterm><primary>PQfsize</primary></indexterm></term>
 
@@ -4037,12 +4227,37 @@ <title>Retrieving Query Result Information</title>
 
       <para>
        This function is only useful when inspecting the result of
-       <xref linkend="libpq-PQdescribePrepared"/>.  For other types of queries it
+       <xref linkend="libpq-PQdescribePrepared"/>.  For other types of results it
        will return zero.
       </para>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQparamisencrypted">
+     <term><function>PQparamisencrypted</function><indexterm><primary>PQparamisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given parameter is destined for an
+       encrypted column.  Parameter numbers start at 0.
+<synopsis>
+int PQparamisencrypted(const PGresult *res, int param_number);
+</synopsis>
+      </para>
+
+      <para>
+       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 <xref linkend="libpq-PQexecPrepared2"/> for another way to do that.)
+       This function is only useful when inspecting the result of <xref
+       linkend="libpq-PQdescribePrepared"/>.  For other types of results it
+       will return false.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQprint">
      <term><function>PQprint</function><indexterm><primary>PQprint</primary></indexterm></term>
 
@@ -4568,6 +4783,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPrepared2"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4575,6 +4791,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPrepared2"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4685,6 +4902,46 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPrepared2">
+     <term><function>PQsendQueryPrepared2</function><indexterm><primary>PQsendQueryPrepared2</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Sends a request to execute a prepared statement with given
+       parameters, without waiting for the result(s), with support for encrypted columns.
+<synopsis>
+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);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPrepared2"/> is like <xref
+       linkend="libpq-PQsendQueryPrepared"/> with additional support for encrypted
+       columns.  The parameter <parameter>paramDesc</parameter> must be a
+       result set obtained from <xref linkend="libpq-PQsendDescribePrepared"/> on
+       the same prepared statement.
+      </para>
+
+      <para>
+       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 <xref linkend="libpq-PQexecPrepared2"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4735,6 +4992,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPrepared2"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7767,6 +8025,26 @@ <title>Environment Variables</title>
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCMKLOOKUP</envar></primary>
+      </indexterm>
+      <envar>PGCMKLOOKUP</envar> behaves the same as the <xref
+      linkend="libpq-connect-cmklookup"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCOLUMNENCRYPTION</envar></primary>
+      </indexterm>
+      <envar>PGCOLUMNENCRYPTION</envar> behaves the same as the <xref
+      linkend="libpq-connect-column-encryption"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 75caa7fdb679..6aa66058d300 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1108,6 +1108,68 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-transparent-column-encryption-flow">
+   <title>Transparent Column Encryption</title>
+
+   <para>
+    Transparent column encryption is enabled by sending the parameter
+    <literal>_pq_.column_encryption</literal> with a value of
+    <literal>1</literal> in the StartupMessage.  This is a protocol extension
+    that enables a few additional protocol messages and adds additional fields
+    to existing protocol messages.  Client drivers should only activate this
+    protocol extension when requested by the user, for example through a
+    connection parameter.
+   </para>
+
+   <para>
+    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.)
+   </para>
+
+   <para>
+    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.
+   </para>
+
+   <para>
+    When column encryption is used, the plaintext is always in text format
+    (not binary format).
+   </para>
+
+   <para>
+    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 <literal>100</literal> and
+    an integer represented by the string <literal>+100</literal> would result
+    in two different ciphertexts, thus defeating the main point of
+    deterministic encryption.  This protocol specification requires the
+    plaintext to be in <quote>canonical</quote> form, which is the form that
+    is produced by the server when it outputs a particular value in text
+    format.
+   </para>
+
+   <para>
+    When transparent column encryption is enabled, the client encoding must
+    match the server encoding.  This ensures that all values encrypted or
+    decrypted by the client match the server encoding.
+   </para>
+
+   <para>
+    The cryptographic operations used for transparent column encryption are
+    described in <xref linkend="protocol-transparent-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2>
    <title>Function Call</title>
 
@@ -4055,6 +4117,140 @@ <title>Message Formats</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ColumnEncryptionKey">
+    <term>ColumnEncryptionKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-transparent-column-encryption-flow"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('Y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column encryption key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the master key used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         The identifier of the algorithm used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The length of the following key material.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Byte<replaceable>n</replaceable></term>
+       <listitem>
+        <para>
+         The key material, encrypted with the master key referenced above.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="protocol-message-formats-ColumnMasterKey">
+    <term>ColumnMasterKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-transparent-column-encryption-flow"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column master key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The key's realm.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="protocol-message-formats-CommandComplete">
     <term>CommandComplete (B)</term>
     <listitem>
@@ -5151,6 +5347,46 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref
+      linkend="protocol-transparent-column-encryption-flow"/>), then there is
+      also the following for each parameter:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         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.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5539,6 +5775,35 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref
+      linkend="protocol-transparent-column-encryption-flow"/>), then there is
+      also the following for each field:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encrypt algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7342,6 +7607,133 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-transparent-column-encryption-crypto">
+  <title>Transparent Column Encryption Cryptography</title>
+
+  <para>
+   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.
+  </para>
+
+  <para>
+   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.
+  </para>
+
+  <sect2 id="protocol-cmk">
+   <title>Column Master Keys</title>
+
+   <para>
+    The currently defined algorithms for column master keys are listed in
+    <xref linkend="protocol-cmk-table"/>.
+   </para>
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="3">
+     <thead>
+      <row>
+       <entry>ID</entry>
+       <entry>Name</entry>
+       <entry>Reference</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/rfc8017">RFC 8017</ulink> (PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/rfc8017">RFC 8017</ulink> (PKCS #1)</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect2>
+
+  <sect2 id="protocol-cek">
+   <title>Column Encryption Keys</title>
+
+   <para>
+    The currently defined algorithms for column encryption keys are listed in
+    <xref linkend="protocol-cek-table"/>.
+   </para>
+
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="3">
+     <thead>
+      <row>
+       <entry>ID</entry>
+       <entry>Name</entry>
+       <entry>Reference</entry>
+       <entry>Key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>130</entry>
+       <entry><literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>32</entry>
+      </row>
+      <row>
+       <entry>131</entry>
+       <entry><literal>AEAD_AES_192_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>132</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>56</entry>
+      </row>
+      <row>
+       <entry>133</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_512</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry;
+       <entry>64</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <para>
+    The <quote>associated data</quote> in these algorithms consists of 4
+    bytes: The ASCII letters <literal>P</literal> and <literal>G</literal>
+    (byte values 80 and 71), followed by the algorithm ID as a 16-bit unsigned
+    integer in network byte order.
+   </para>
+
+   <para>
+    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
+<programlisting>
+SUBSTRING(<replaceable>HMAC</replaceable>(<replaceable>K</replaceable>, <replaceable>P</replaceable>) FOR <replaceable>IVLEN</replaceable>)
+</programlisting>
+    where <replaceable>HMAC</replaceable> is the HMAC function associated with
+    the algorithm, <replaceable>K</replaceable> is the prefix of the CEK of
+    the length required by the HMAC function, and <replaceable>P</replaceable>
+    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.)
+   </para>
+  </sect2>
+ </sect1>
+
  <sect1 id="protocol-changes">
   <title>Summary of Changes since Protocol 2.0</title>
 
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index e90a0e1f8372..331b1f010b5c 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -8,6 +8,8 @@
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
+<!ENTITY alterColumnEncryptionKey SYSTEM "alter_column_encryption_key.sgml">
+<!ENTITY alterColumnMasterKey SYSTEM "alter_column_master_key.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
 <!ENTITY alterDefaultPrivileges SYSTEM "alter_default_privileges.sgml">
@@ -62,6 +64,8 @@
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
+<!ENTITY createColumnEncryptionKey SYSTEM "create_column_encryption_key.sgml">
+<!ENTITY createColumnMasterKey SYSTEM "create_column_master_key.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
 <!ENTITY createDatabase     SYSTEM "create_database.sgml">
 <!ENTITY createDomain       SYSTEM "create_domain.sgml">
@@ -109,6 +113,8 @@
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
+<!ENTITY dropColumnEncryptionKey SYSTEM "drop_column_encryption_key.sgml">
+<!ENTITY dropColumnMasterKey SYSTEM "drop_column_master_key.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
 <!ENTITY dropDatabase       SYSTEM "drop_database.sgml">
 <!ENTITY dropDomain         SYSTEM "drop_domain.sgml">
diff --git a/doc/src/sgml/ref/alter_column_encryption_key.sgml b/doc/src/sgml/ref/alter_column_encryption_key.sgml
new file mode 100644
index 000000000000..7597cd80ca6a
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,186 @@
+<!--
+doc/src/sgml/ref/alter_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-encryption-key">
+ <indexterm zone="sql-alter-column-encryption-key">
+  <primary>ALTER COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>change the definition of a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> ADD VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> DROP VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN ENCRYPTION KEY</command> changes the definition of a
+   column encryption key.
+  </para>
+
+  <para>
+   The first form adds new encrypted key data to a column encryption key,
+   which must be encrypted with a different column master key than the
+   existing key data.  The second form removes a key data entry for a given
+   column master key.  Together, these forms can be used for column master key
+   rotation.
+  </para>
+
+  <para>
+   You must own the column encryption key to use <command>ALTER COLUMN
+   ENCRYPTION KEY</command>.  To alter the owner, you must also be a direct or
+   indirect member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column encryption key's
+   database.  (These restrictions enforce that altering the owner doesn't do
+   anything you couldn't do by dropping and recreating the column encryption
+   key.  However, a superuser can alter ownership of any column encryption key
+   anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name of an existing column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  See <xref
+      linkend="sql-create-column-encryption-key"/> for details
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rotate the master keys used to encrypt a given column encryption key,
+   use a command sequence like this:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (
+    COLUMN_MASTER_KEY = cmk1
+);
+</programlisting>
+  </para>
+
+  <para>
+   To rename the column encryption key <literal>cek1</literal> to
+   <literal>cek2</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column encryption key <literal>cek1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN ENCRYPTION KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/alter_column_master_key.sgml b/doc/src/sgml/ref/alter_column_master_key.sgml
new file mode 100644
index 000000000000..13e310b22748
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,117 @@
+<!--
+doc/src/sgml/ref/alter_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-master-key">
+ <indexterm zone="sql-alter-column-master-key">
+  <primary>ALTER COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN MASTER KEY</refname>
+  <refpurpose>change the definition of a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   You must own the column master key to use <command>ALTER COLUMN MASTER
+   KEY</command>.  To alter the owner, you must also be a direct or indirect
+   member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column master key's database.
+   (These restrictions enforce that altering the owner doesn't do anything you
+   couldn't do by dropping and recreating the column master key.  However, a
+   superuser can alter ownership of any column master key anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the column master key <literal>cmk1</literal> to
+   <literal>cmk2</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column master key <literal>cmk1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN MASTER KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/create_column_encryption_key.sgml b/doc/src/sgml/ref/create_column_encryption_key.sgml
new file mode 100644
index 000000000000..a94b0924b153
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,163 @@
+<!--
+doc/src/sgml/ref/create_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-encryption-key">
+ <indexterm zone="sql-create-column-encryption-key">
+  <primary>CREATE COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>define a new column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN ENCRYPTION KEY <replaceable>name</replaceable> WITH VALUES (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+[ , ... ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN ENCRYPTION KEY</command> defines a new column
+   encryption key.  A column encryption key is used for client-side encryption
+   of table columns that have been defined as encrypted.  The key material of
+   a column encryption key is stored in the database's system catalogs,
+   encrypted (wrapped) by a column master key (which in turn is only
+   accessible to the client, not the database server).
+  </para>
+
+  <para>
+   A column encryption key can be associated with more than one column master
+   key.  To specify that, specify more than one parenthesized definition (see
+   also the examples).
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  Supported algorithms are:
+      <itemizedlist>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal> (default)</para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal> (default)</para>
+       </listitem>
+      </itemizedlist>
+     </para>
+
+     <para>
+      This is informational only.  The specified value is provided to the
+      client, which may use it for decrypting the column encryption key on the
+      client side.  But a client is also free to ignore this information and
+      figure out how to arrange the decryption in some other way.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+</programlisting>
+  </para>
+
+  <para>
+   To specify more than one associated column master key:
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\xF1F2F2F4...'
+);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_column_master_key.sgml b/doc/src/sgml/ref/create_column_master_key.sgml
new file mode 100644
index 000000000000..ec5fa4cde571
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,106 @@
+<!--
+doc/src/sgml/ref/create_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-master-key">
+ <indexterm zone="sql-create-column-master-key">
+  <primary>CREATE COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN MASTER KEY</refname>
+  <refpurpose>define a new column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN MASTER KEY <replaceable>name</replaceable> WITH (
+    [ REALM = <replaceable>realm</replaceable> ]
+)
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN MASTER KEY</command> defines a new column master
+   key.  A column master key is used to encrypt column encryption keys, which
+   are the keys that actually encrypt the column data.  The key material of
+   the column master key is not stored in the database.  The definition of a
+   column master key records information that will allow a client to locate
+   the key material, for example in a file or in a key management system.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>realm</replaceable></term>
+
+    <listitem>
+     <para>
+      This is an optional string that can be used to organize column master
+      keys into groups for lookup by clients.  The intent is that all column
+      master keys that are stored in the same system (file system location,
+      key management system, etc.) should be in the same realm.  A client
+      would then be configured to look up all keys in a given realm in a
+      certain way.  See the documentation of the respective client library for
+      further usage instructions.
+     </para>
+
+     <para>
+      The default is the empty string.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+<programlisting>
+CREATE COLUMN MASTER KEY cmk1 (realm = 'myrealm');
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index c14b2010d81d..feebf51fd9a5 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -22,7 +22,7 @@
  <refsynopsisdiv>
 <synopsis>
 CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable> ( [
-  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -349,6 +349,46 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables transparent column encryption for the column.
+      <replaceable>encryption_options</replaceable> are comma-separated
+      <literal>key=value</literal> specifications.  The following options are
+      available:
+      <variablelist>
+       <varlistentry>
+        <term><literal>column_encryption_key</literal></term>
+        <listitem>
+         <para>
+          Specifies the name of the column encryption key to use.  Specifying
+          this is mandatory.
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>encryption_type</literal></term>
+        <listitem>
+         <para>
+          <literal>randomized</literal> (the default) or <literal>deterministic</literal>
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>algorithm</literal></term>
+        <listitem>
+         <para>
+          The encryption algorithm to use.  The default is
+          <literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
diff --git a/doc/src/sgml/ref/drop_column_encryption_key.sgml b/doc/src/sgml/ref/drop_column_encryption_key.sgml
new file mode 100644
index 000000000000..9d157de9c5f8
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_encryption_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-encryption-key">
+ <indexterm zone="sql-drop-column-encryption-key">
+  <primary>DROP COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>remove a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN ENCRYPTION KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN ENCRYPTION KEY</command> removes a previously defined
+   column encryption key.  To be able to drop a column encryption key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column encryption key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the column encryption key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column encryption key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column encryption key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN ENCRYPTION KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_column_master_key.sgml b/doc/src/sgml/ref/drop_column_master_key.sgml
new file mode 100644
index 000000000000..c85098ea1c99
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_master_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-master-key">
+ <indexterm zone="sql-drop-column-master-key">
+  <primary>DROP COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN MASTER KEY</refname>
+  <refpurpose>remove a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN MASTER KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN MASTER KEY</command> removes a previously defined
+   column master key.  To be able to drop a column master key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column master key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the column master key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column master key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column master key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN MASTER KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 8b9d9f4cad43..7204971e1a12 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -691,6 +691,37 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  Note that a dump created
+        with this option cannot be restored into a database with column
+        encryption.
+       </para>
+       <!--
+           XXX: The latter would require another pg_dump option to put out
+           INSERT ... \gencr commands.
+       -->
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index e62d05e5ab58..4bf60c729f7f 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -259,6 +259,33 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  Note that a dump created
+        with this option cannot be restored into a database with column
+        encryption.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 9494f28063ad..cd61e99e86fe 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2255,6 +2255,28 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry>
+       <term><literal>\gencr</literal> [ <replaceable class="parameter">parameter</replaceable> ] ... </term>
+
+       <listitem>
+        <para>
+         Sends the current query buffer to the server for execution, as with
+         <literal>\g</literal>, with the specified parameters passed for any
+         parameter placeholders (<literal>$1</literal> etc.).  This command
+         ensures that any parameters corresponding to encrypted columns are
+         sent to the server encrypted.
+        </para>
+
+        <para>
+         Example:
+<programlisting>
+INSERT INTO tbl1 VALUES ($1, $2) \gencr 'first value' 'second value'
+</programlisting>
+        </para>
+       </listitem>
+      </varlistentry>
+
+
       <varlistentry>
         <term><literal>\getenv <replaceable class="parameter">psql_var</replaceable> <replaceable class="parameter">env_var</replaceable></literal></term>
 
@@ -3978,6 +4000,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>HIDE_COLUMN_ENCRYPTION</varname></term>
+        <listitem>
+        <para>
+         If this variable is set to <literal>true</literal>, column encryption
+         details are not displayed. This is mainly useful for regression
+         tests.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index a3b743e8c1e7..e1425c222fab 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -36,6 +36,8 @@ <title>SQL Commands</title>
    &abort;
    &alterAggregate;
    &alterCollation;
+   &alterColumnEncryptionKey;
+   &alterColumnMasterKey;
    &alterConversion;
    &alterDatabase;
    &alterDefaultPrivileges;
@@ -90,6 +92,8 @@ <title>SQL Commands</title>
    &createAggregate;
    &createCast;
    &createCollation;
+   &createColumnEncryptionKey;
+   &createColumnMasterKey;
    &createConversion;
    &createDatabase;
    &createDomain;
@@ -137,6 +141,8 @@ <title>SQL Commands</title>
    &dropAggregate;
    &dropCast;
    &dropCollation;
+   &dropColumnEncryptionKey;
+   &dropColumnMasterKey;
    &dropConversion;
    &dropDatabase;
    &dropDomain;
diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index c99ae54cb026..86482eb4c81c 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -20,7 +20,9 @@
 
 #include "access/printsimple.h"
 #include "catalog/pg_type.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 
 /*
@@ -46,6 +48,11 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc)
 		pq_sendint16(&buf, attr->attlen);
 		pq_sendint32(&buf, attr->atttypmod);
 		pq_sendint16(&buf, 0);	/* format code */
+		if (MyProcPort->column_encryption_enabled)
+		{
+			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 d2f3b5728846..80ab5e875bd0 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,28 @@
  */
 #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/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.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 +166,139 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 	 */
 }
 
+/*
+ * Send ColumnMasterKey message, unless it's already been sent in this session
+ * for this key.
+ */
+List	   *cmk_sent = NIL;
+
+static void
+cmk_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+}
+
+static void
+MaybeSendColumnMasterKeyMessage(Oid cmkid)
+{
+	HeapTuple	tuple;
+	Form_pg_colmasterkey cmkform;
+	Datum		datum;
+	bool		isnull;
+	StringInfoData buf;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	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);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CMKOID, cmk_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	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.
+ */
+List	   *cek_sent = NIL;
+
+static void
+cek_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
+void
+MaybeSendColumnEncryptionKeyMessage(Oid attcek)
+{
+	HeapTuple	tuple;
+	ScanKeyData skey[1];
+	SysScanDesc sd;
+	Relation	rel;
+	bool		found = false;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	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);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CEKDATAOID, cek_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cek_sent = lappend_oid(cek_sent, attcek);
+	MemoryContextSwitchTo(oldcontext);
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -167,6 +315,7 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 						  List *targetlist, int16 *formats)
 {
 	int			natts = typeinfo->natts;
+	size_t		sz;
 	int			i;
 	ListCell   *tlist_item = list_head(targetlist);
 
@@ -183,14 +332,17 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 	 * Have to overestimate the size of the column-names, to account for
 	 * character set overhead.
 	 */
-	enlargeStringInfo(buf, (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
-							+ sizeof(Oid)	/* resorigtbl */
-							+ sizeof(AttrNumber)	/* resorigcol */
-							+ sizeof(Oid)	/* atttypid */
-							+ sizeof(int16) /* attlen */
-							+ sizeof(int32) /* attypmod */
-							+ sizeof(int16) /* format */
-							) * natts);
+	sz = (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
+		  + sizeof(Oid)	/* resorigtbl */
+		  + sizeof(AttrNumber)	/* resorigcol */
+		  + sizeof(Oid)	/* atttypid */
+		  + sizeof(int16) /* attlen */
+		  + sizeof(int32) /* attypmod */
+		  + sizeof(int16)); /* format */
+	if (MyProcPort->column_encryption_enabled)
+		sz += (sizeof(int32)		/* attcekid */
+			   + sizeof(int16));	/* attencalg */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +352,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 +385,22 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(atttypid))
+		{
+			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 +408,11 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		pq_writeint16(buf, att->attlen);
 		pq_writeint32(buf, atttypmod);
 		pq_writeint16(buf, format);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			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 d6fb261e2016..e3ecc64ea7a5 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 10bf26ce7c07..cea91d4a88a3 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 89a0221ec9b8..7e1a91b9749c 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/aclchk.c b/src/backend/catalog/aclchk.c
index aa5a2ed9483e..31978330c3f7 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -32,7 +32,9 @@
 #include "catalog/pg_am.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_conversion.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_default_acl.h"
@@ -3577,6 +3579,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEK:
+					case OBJECT_CEKDATA:
+					case OBJECT_CMK:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3607,6 +3612,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("must be owner of aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("must be owner of column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("must be owner of column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("must be owner of collation %s");
 						break;
@@ -3717,6 +3728,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -5580,6 +5592,58 @@ pg_collation_ownercheck(Oid coll_oid, Oid roleid)
 	return has_privs_of_role(roleid, ownerId);
 }
 
+/*
+ * Ownership check for a column encryption key (specified by OID).
+ */
+bool
+pg_column_encryption_key_ownercheck(Oid cek_oid, Oid roleid)
+{
+	HeapTuple	tuple;
+	Oid			ownerId;
+
+	/* Superusers bypass all permission checking. */
+	if (superuser_arg(roleid))
+		return true;
+
+	tuple = SearchSysCache1(CEKOID, ObjectIdGetDatum(cek_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key with OID %u does not exist", cek_oid)));
+
+	ownerId = ((Form_pg_colenckey) GETSTRUCT(tuple))->cekowner;
+
+	ReleaseSysCache(tuple);
+
+	return has_privs_of_role(roleid, ownerId);
+}
+
+/*
+ * Ownership check for a column master key (specified by OID).
+ */
+bool
+pg_column_master_key_ownercheck(Oid cmk_oid, Oid roleid)
+{
+	HeapTuple	tuple;
+	Oid			ownerId;
+
+	/* Superusers bypass all permission checking. */
+	if (superuser_arg(roleid))
+		return true;
+
+	tuple = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmk_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key with OID %u does not exist", cmk_oid)));
+
+	ownerId = ((Form_pg_colmasterkey) GETSTRUCT(tuple))->cmkowner;
+
+	ReleaseSysCache(tuple);
+
+	return has_privs_of_role(roleid, ownerId);
+}
+
 /*
  * Ownership check for a conversion (specified by OID).
  */
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 39768fa22be1..6f1ea268f034 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -30,7 +30,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.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"
@@ -153,6 +156,9 @@ static const Oid object_classes[] = {
 	TypeRelationId,				/* OCLASS_TYPE */
 	CastRelationId,				/* OCLASS_CAST */
 	CollationRelationId,		/* OCLASS_COLLATION */
+	ColumnEncKeyRelationId,		/* OCLASS_CEK */
+	ColumnEncKeyDataRelationId,	/* OCLASS_CEKDATA */
+	ColumnMasterKeyRelationId,	/* OCLASS_CMK */
 	ConstraintRelationId,		/* OCLASS_CONSTRAINT */
 	ConversionRelationId,		/* OCLASS_CONVERSION */
 	AttrDefaultRelationId,		/* OCLASS_DEFAULT */
@@ -1487,6 +1493,9 @@ doDeletion(const ObjectAddress *object, int flags)
 
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONVERSION:
 		case OCLASS_LANGUAGE:
 		case OCLASS_OPCLASS:
@@ -2859,6 +2868,15 @@ getObjectClass(const ObjectAddress *object)
 		case CollationRelationId:
 			return OCLASS_COLLATION;
 
+		case ColumnEncKeyRelationId:
+			return OCLASS_CEK;
+
+		case ColumnEncKeyDataRelationId:
+			return OCLASS_CEKDATA;
+
+		case ColumnMasterKeyRelationId:
+			return OCLASS_CMK;
+
 		case ConstraintRelationId:
 			return OCLASS_CONSTRAINT;
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 9a80ccdccdf4..b65f194069e3 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -749,6 +749,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/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 284ca55469e0..62fdb6d70a70 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.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"
@@ -191,6 +194,48 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_COLLATION,
 		true
 	},
+	{
+		"column encryption key",
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		CEKOID,
+		CEKNAME,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		InvalidAttrNumber,
+		Anum_pg_colenckey_cekowner,
+		InvalidAttrNumber,
+		OBJECT_CEK,
+		true
+	},
+	{
+		"column encryption key data",
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		CEKDATAOID,
+		-1,
+		Anum_pg_colenckeydata_oid,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		-1,
+		false
+	},
+	{
+		"column master key",
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		CMKOID,
+		CMKNAME,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		InvalidAttrNumber,
+		Anum_pg_colmasterkey_cmkowner,
+		InvalidAttrNumber,
+		OBJECT_CMK,
+		true
+	},
 	{
 		"constraint",
 		ConstraintRelationId,
@@ -723,6 +768,18 @@ static const struct object_type_map
 	{
 		"collation", OBJECT_COLLATION
 	},
+	/* OCLASS_CEK */
+	{
+		"column encryption key", OBJECT_CEK
+	},
+	/* OCLASS_CEKDATA */
+	{
+		"column encryption key data", OBJECT_CEKDATA
+	},
+	/* OCLASS_CMK */
+	{
+		"column master key", OBJECT_CMK
+	},
 	/* OCLASS_CONSTRAINT */
 	{
 		"table constraint", OBJECT_TABCONSTRAINT
@@ -1028,6 +1085,8 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+			case OBJECT_CMK:
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1107,6 +1166,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					char	   *cekname = strVal(linitial(castNode(List, object)));
+					char	   *cmkname = strVal(lsecond(castNode(List, object)));
+					Oid			cekid;
+					Oid			cmkid;
+
+					cekid = get_cek_oid(cekname, missing_ok);
+					cmkid = get_cmk_oid(cmkname, missing_ok);
+
+					address.classId = ColumnEncKeyDataRelationId;
+					address.objectId = get_cekdata_oid(cekid, cmkid, missing_ok);
+					address.objectSubId = 0;
+				}
+				break;
 			case OBJECT_TRANSFORM:
 				{
 					TypeName   *typename = linitial_node(TypeName, castNode(List, object));
@@ -1294,6 +1368,16 @@ get_object_address_unqualified(ObjectType objtype,
 			address.objectId = get_am_oid(name, missing_ok);
 			address.objectSubId = 0;
 			break;
+		case OBJECT_CEK:
+			address.classId = ColumnEncKeyRelationId;
+			address.objectId = get_cek_oid(name, missing_ok);
+			address.objectSubId = 0;
+			break;
+		case OBJECT_CMK:
+			address.classId = ColumnMasterKeyRelationId;
+			address.objectId = get_cmk_oid(name, missing_ok);
+			address.objectSubId = 0;
+			break;
 		case OBJECT_DATABASE:
 			address.classId = DatabaseRelationId;
 			address.objectId = get_database_oid(name, missing_ok);
@@ -2254,6 +2338,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 	 */
 	switch (type)
 	{
+		case OBJECT_CEKDATA:
 		case OBJECT_PUBLICATION_NAMESPACE:
 		case OBJECT_USER_MAPPING:
 			if (list_length(name) != 1)
@@ -2328,6 +2413,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			objnode = (Node *) name;
 			break;
 		case OBJECT_ACCESS_METHOD:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_DATABASE:
 		case OBJECT_EVENT_TRIGGER:
 		case OBJECT_EXTENSION:
@@ -2358,6 +2445,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_PUBLICATION_REL:
 			objnode = (Node *) list_make2(name, linitial(args));
 			break;
+		case OBJECT_CEKDATA:
 		case OBJECT_PUBLICATION_NAMESPACE:
 		case OBJECT_USER_MAPPING:
 			objnode = (Node *) list_make2(linitial(name), linitial(args));
@@ -2635,6 +2723,16 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   NameListToString(castNode(List, object)));
 			break;
+		case OBJECT_CEK:
+			if (!pg_column_encryption_key_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+							   strVal(object));
+			break;
+		case OBJECT_CMK:
+			if (!pg_column_master_key_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+							   strVal(object));
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d",
 				 (int) objtype);
@@ -3095,6 +3193,48 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				char	   *cekname = get_cek_name(object->objectId, missing_ok);
+
+				if (cekname)
+					appendStringInfo(&buffer, _("column encryption key %s"), cekname);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata cekdata;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+
+				cekdata = (Form_pg_colenckeydata) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("column encryption key %s data for master key %s"),
+								 get_cek_name(cekdata->ckdcekid, false),
+								 get_cmk_name(cekdata->ckdcmkid, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				char	   *cmkname = get_cmk_name(object->objectId, missing_ok);
+
+				if (cmkname)
+					appendStringInfo(&buffer, _("column master key %s"), cmkname);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4519,6 +4659,18 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "collation");
 			break;
 
+		case OCLASS_CEK:
+			appendStringInfoString(&buffer, "column encryption key");
+			break;
+
+		case OCLASS_CEKDATA:
+			appendStringInfoString(&buffer, "column encryption key data");
+			break;
+
+		case OCLASS_CMK:
+			appendStringInfoString(&buffer, "column master key");
+			break;
+
 		case OCLASS_CONSTRAINT:
 			getConstraintTypeDescription(&buffer, object->objectId,
 										 missing_ok);
@@ -4984,6 +5136,74 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+
+				tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+				appendStringInfoString(&buffer,
+									   quote_identifier(NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make1(pstrdup(NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata form;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckeydata) GETSTRUCT(tup);
+				appendStringInfo(&buffer,
+								 "of %s for %s", get_cek_name(form->ckdcekid, false), get_cmk_name(form->ckdcmkid, false));
+				if (objname)
+					*objname = list_make1(get_cek_name(form->ckdcekid, false));
+				if (objargs)
+					*objargs = list_make1(get_cmk_name(form->ckdcmkid, false));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+
+				tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column master key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+				appendStringInfoString(&buffer,
+									   quote_identifier(NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make1(pstrdup(NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91c7..69f6175c60e0 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -19,6 +19,7 @@ OBJS = \
 	analyze.o \
 	async.o \
 	cluster.o \
+	colenccmds.o \
 	collationcmds.o \
 	comment.o \
 	constraint.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 55219bb0974a..6caa21e325fb 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -22,7 +22,9 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_foreign_data_wrapper.h"
@@ -80,6 +82,12 @@ report_name_conflict(Oid classId, const char *name)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists");
+			break;
 		case EventTriggerRelationId:
 			msgfmt = gettext_noop("event trigger \"%s\" already exists");
 			break;
@@ -375,6 +383,8 @@ ExecRenameStmt(RenameStmt *stmt)
 			return RenameType(stmt);
 
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_EVENT_TRIGGER:
@@ -639,6 +649,9 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 			break;
 
 		case OCLASS_CAST:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
@@ -872,6 +885,8 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/colenccmds.c b/src/backend/commands/colenccmds.c
new file mode 100644
index 000000000000..bef8f00b9834
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,354 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/colenccmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+static void
+parse_cek_attributes(ParseState *pstate, List *definition, Oid *cmkoid_p, int16 *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = 0;
+	int16		alg;
+	char	   *encval;
+
+	Assert(cmkoid_p);
+
+	foreach(lc, definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "column_master_key") == 0)
+			defelp = &cmkEl;
+		else if (strcmp(defel->defname, "algorithm") == 0)
+			defelp = &algEl;
+		else if (strcmp(defel->defname, "encrypted_value") == 0)
+			defelp = &encvalEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column encryption key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (cmkEl)
+	{
+		char	   *val = defGetString(cmkEl);
+
+		cmkoid = GetSysCacheOid1(CMKNAME, Anum_pg_colmasterkey_oid, PointerGetDatum(val));
+		if (!cmkoid)
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_OBJECT),
+					errmsg("column master key \"%s\" does not exist", val));
+	}
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("attribute \"%s\" must be specified",
+						"column_master_key")));
+
+	if (algEl)
+	{
+		char	   *val = defGetString(algEl);
+
+		if (!alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"algorithm")));
+
+		if (strcmp(val, "RSAES_OAEP_SHA_1") == 0)
+			alg = PG_CMK_RSAES_OAEP_SHA_1;
+		else if (strcmp(val, "PG_CMK_RSAES_OAEP_SHA_256") == 0)
+			alg = PG_CMK_RSAES_OAEP_SHA_256;
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+		alg = PG_CMK_RSAES_OAEP_SHA_1;
+
+	if (encvalEl)
+	{
+		if (!encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"encrypted_value")));
+
+		encval = defGetString(encvalEl);
+	}
+	else
+	{
+		if (encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"encrypted_value")));
+	}
+
+	*cmkoid_p = cmkoid;
+	if (alg_p)
+		*alg_p = alg;
+	if (encval_p)
+		*encval_p = encval;
+}
+
+static void
+insert_cekdata_record(Oid cekoid, Oid cmkoid, int16 alg, char *encval)
+{
+	Oid			cekdataoid;
+	Relation	rel;
+	Datum		values[Natts_pg_colenckeydata] = {0};
+	bool		nulls[Natts_pg_colenckeydata] = {0};
+	HeapTuple	tup;
+	ObjectAddress myself;
+	ObjectAddress other;
+
+	rel = table_open(ColumnEncKeyDataRelationId, RowExclusiveLock);
+
+	cekdataoid = GetNewOidWithIndex(rel, ColumnEncKeyDataOidIndexId, Anum_pg_colenckeydata_oid);
+	values[Anum_pg_colenckeydata_oid - 1] = ObjectIdGetDatum(cekdataoid);
+	values[Anum_pg_colenckeydata_ckdcekid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckeydata_ckdcmkid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colenckeydata_ckdcmkalg - 1] = Int16GetDatum(alg);
+	values[Anum_pg_colenckeydata_ckdencval - 1] = DirectFunctionCall1(byteain, CStringGetDatum(encval));
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyDataRelationId, cekdataoid);
+
+	/* dependency cekdata -> cek */
+	ObjectAddressSet(other, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_AUTO);
+
+	/* dependency cekdata -> cmk */
+	ObjectAddressSet(other, ColumnMasterKeyRelationId, cmkoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_NORMAL);
+
+	table_close(rel, NoLock);
+}
+
+ObjectAddress
+CreateCEK(ParseState *pstate, DefineStmt *stmt)
+{
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	aclresult = pg_database_aclcheck(MyDatabaseId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(MyDatabaseId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	cekoid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid,
+							 CStringGetDatum(strVal(llast(stmt->defnames))));
+	if (OidIsValid(cekoid))
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_OBJECT),
+				 errmsg("column encryption key \"%s\" already exists",
+						strVal(llast(stmt->defnames)))));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int16		alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] =
+		DirectFunctionCall1(namein, CStringGetDatum(strVal(llast(stmt->defnames))));
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOnOwner(ColumnEncKeyRelationId, cekoid, GetUserId());
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnEncKeyRelationId, cekoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt)
+{
+	Oid			cekoid;
+	ObjectAddress address;
+
+	cekoid = get_cek_oid(stmt->cekname, false);
+
+	if (!pg_column_encryption_key_ownercheck(cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, stmt->cekname);
+
+	if (stmt->isDrop)
+	{
+		Oid			cmkoid = 0;
+		Oid			cekdataoid;
+		ObjectAddress obj;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, NULL, NULL);
+		cekdataoid = get_cekdata_oid(cekoid, cmkoid, false);
+		ObjectAddressSet(obj, ColumnEncKeyDataRelationId, cekdataoid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+		// TODO: prevent deleting all data entries for a key?
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int16		alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+		if (get_cekdata_oid(cekoid, cmkoid, true))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("column encryption key \"%s\" already has data for master key \"%s\"",
+						   stmt->cekname, get_cmk_name(cmkoid, false)));
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	InvokeObjectPostAlterHook(ColumnEncKeyRelationId, cekoid, 0);
+	ObjectAddressSet(address, ColumnEncKeyRelationId, cekoid);
+
+	return address;
+}
+
+ObjectAddress
+CreateCMK(ParseState *pstate, DefineStmt *stmt)
+{
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	aclresult = pg_database_aclcheck(MyDatabaseId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(MyDatabaseId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	cmkoid = GetSysCacheOid1(CMKNAME, Anum_pg_colmasterkey_oid,
+							 CStringGetDatum(strVal(llast(stmt->defnames))));
+	if (OidIsValid(cmkoid))
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_OBJECT),
+				 errmsg("column master key \"%s\" already exists",
+						strVal(llast(stmt->defnames)))));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+		realm = defGetString(realmEl);
+	else
+		realm = "";
+
+	cmkoid = GetNewOidWithIndex(rel, ColumnMasterKeyOidIndexId, Anum_pg_colmasterkey_oid);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] =
+		DirectFunctionCall1(namein, CStringGetDatum(strVal(llast(stmt->defnames))));
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	recordDependencyOnOwner(ColumnMasterKeyRelationId, cmkoid, GetUserId());
+
+	ObjectAddressSet(myself, ColumnMasterKeyRelationId, cmkoid);
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	return myself;
+}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 441f29d684ff..51b8686d4632 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,9 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -1027,6 +1030,9 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_TYPE:
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_CONVERSION:
 		case OCLASS_DEFAULT:
@@ -2056,6 +2062,9 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2139,6 +2148,9 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 9b350d025ffc..6e26e158c449 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -5,6 +5,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index c4b54d054757..65dde324419e 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 = palloc0_array(Oid, nargs);
+		argorigcols = palloc0_array(AttrNumber, nargs);
 	}
 
 	/*
@@ -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,34 +691,49 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 		hash_seq_init(&hash_seq, prepared_queries);
 		while ((prep_stmt = hash_seq_search(&hash_seq)) != NULL)
 		{
+			int			num_params = prep_stmt->plansource->num_params;
 			TupleDesc	result_desc;
-			Datum		values[8];
-			bool		nulls[8] = {0};
+			Datum	   *tmp_ary;
+			Datum		values[10];
+			bool		nulls[10] = {0};
 
 			result_desc = prep_stmt->plansource->resultDesc;
 
 			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 = palloc_array(Datum, num_params);
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_types[i]);
+			values[3] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGTYPEOID));
+
+			tmp_ary = palloc_array(Datum, num_params);
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_origtbls[i]);
+			values[4] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGCLASSOID));
+
+			tmp_ary = palloc_array(Datum, num_params);
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = Int16GetDatum(prep_stmt->plansource->param_origcols[i]);
+			values[5] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, INT2OID));
+
 			if (result_desc)
 			{
-				Oid		   *result_types;
-
-				result_types = palloc_array(Oid, result_desc->natts);
+				tmp_ary = palloc_array(Datum, result_desc->natts);
 				for (int i = 0; i < result_desc->natts; i++)
-					result_types[i] = result_desc->attrs[i].atttypid;
-				values[4] = build_regtype_array(result_types, result_desc->natts);
+					tmp_ary[i] = ObjectIdGetDatum(result_desc->attrs[i].atttypid);
+				values[6] = PointerGetDatum(construct_array_builtin(tmp_ary, result_desc->natts, REGTYPEOID));
 			}
 			else
 			{
 				/* no result descriptor (for example, DML statement) */
-				nulls[4] = true;
+				nulls[6] = true;
 			}
-			values[5] = BoolGetDatum(prep_stmt->from_sql);
-			values[6] = Int64GetDatumFast(prep_stmt->plansource->num_generic_plans);
-			values[7] = Int64GetDatumFast(prep_stmt->plansource->num_custom_plans);
+
+			values[7] = BoolGetDatum(prep_stmt->from_sql);
+			values[8] = Int64GetDatumFast(prep_stmt->plansource->num_generic_plans);
+			values[9] = Int64GetDatumFast(prep_stmt->plansource->num_custom_plans);
 
 			tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
 								 values, nulls);
@@ -719,24 +742,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 = palloc_array(Datum, num_params);
-
-	for (i = 0; i < num_params; i++)
-		tmp_ary[i] = ObjectIdGetDatum(param_types[i]);
-
-	result = construct_array_builtin(tmp_ary, num_params, REGTYPEOID);
-	return PointerGetDatum(result);
-}
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ae19b98bb9a..07ad646a520c 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,9 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7d8a75d23c2d..e9b50e333a4f 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"
@@ -636,6 +637,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr);
 
 
 /* ----------------------------------------------------------------
@@ -935,6 +937,9 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+			GetColumnEncryption(colDef->encryption, attr);
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -6870,6 +6875,14 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attislocal = colDef->is_local;
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
+	if (colDef->encryption)
+		GetColumnEncryption(colDef->encryption, &attribute);
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attrealtypid = 0;
+		attribute.attencalg = 0;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12664,6 +12677,9 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
 			case OCLASS_COLLATION:
+			case OCLASS_CEK:
+			case OCLASS_CEKDATA:
+			case OCLASS_CMK:
 			case OCLASS_CONVERSION:
 			case OCLASS_LANGUAGE:
 			case OCLASS_LARGEOBJECT:
@@ -19299,3 +19315,83 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	char	   *cek = NULL;
+	Oid			cekoid;
+	bool		encdet = false;
+	int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+	foreach(lc, coldefencryption)
+	{
+		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;
+	attr->attencalg = alg;
+
+	/* override physical type */
+	if (encdet)
+		attr->atttypid = PG_ENCRYPTED_DETOID;
+	else
+		attr->atttypid = PG_ENCRYPTED_RNDOID;
+	get_typlenbyvalalign(attr->atttypid,
+						 &attr->attlen, &attr->attbyval, &attr->attalign);
+	attr->attstorage = get_typstorage(attr->atttypid);
+	attr->attcollation = InvalidOid;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index c795cb7a29ce..6cad27bf597b 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -25,6 +25,7 @@
 #include "access/xlogprefetcher.h"
 #include "catalog/pg_authid.h"
 #include "common/string.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "postmaster/postmaster.h"
@@ -706,7 +707,11 @@ check_client_encoding(char **newval, void **extra, GucSource source)
 	 */
 	if (PrepareClientEncoding(encoding) < 0)
 	{
-		if (IsTransactionState())
+		if (MyProcPort->column_encryption_enabled)
+			GUC_check_errdetail("Conversion between %s and %s is not possible when column encryption is enabled.",
+								canonical_name,
+								GetDatabaseEncodingName());
+		else if (IsTransactionState())
 		{
 			/* Must be a genuine no-such-conversion problem */
 			GUC_check_errcode(ERRCODE_FEATURE_NOT_SUPPORTED);
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 29bc26669b00..cde5b0e0870b 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/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 0a7b22f97e7b..a549b985d365 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4006,6 +4006,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(coldef->compression))
 					return true;
+				if (WALK(coldef->encryption))
+					return true;
 				if (WALK(coldef->raw_default))
 					return true;
 				if (WALK(coldef->collClause))
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 6688c2a865b8..ce9b7a5f5f38 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 0d8d2928509c..190fb66aa91c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,7 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 }
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
-		AlterEventTrigStmt AlterCollationStmt
+		AlterEventTrigStmt AlterCollationStmt AlterColumnEncryptionKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -420,6 +420,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <list>	parse_toplevel stmtmulti routine_body_stmt_list
 				OptTableElementList TableElementList OptInherit definition
+				list_of_definitions
 				OptTypedTableElementList TypedTableElementList
 				reloptions opt_reloptions
 				OptWith opt_definition func_args func_args_list
@@ -593,6 +594,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableConstraint TableLikeClause
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression column_storage opt_column_storage
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_match
@@ -691,8 +693,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
 	DOUBLE_P DROP
 
-	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
-	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EACH ELSE ENABLE_P ENCODING ENCRYPTION ENCRYPTED END_P ENUM_P ESCAPE
+	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -715,7 +717,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
-	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+	MAPPING MASTER MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
 
 	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -943,6 +945,7 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -3681,14 +3684,15 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_storage opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_encryption opt_column_storage opt_column_compression create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 
 					n->colname = $1;
 					n->typeName = $2;
-					n->storage_name = $3;
-					n->compression = $4;
+					n->encryption = $3;
+					n->storage_name = $4;
+					n->compression = $5;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3697,8 +3701,8 @@ columnDef:	ColId Typename opt_column_storage opt_column_compression create_gener
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $5;
-					SplitColQualList($6, &n->constraints, &n->collClause,
+					n->fdwoptions = $6;
+					SplitColQualList($7, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *) n;
@@ -3755,6 +3759,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 		;
@@ -6250,6 +6259,24 @@ DefineStmt:
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+			| CREATE COLUMN ENCRYPTION KEY any_name WITH VALUES list_of_definitions
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CEK;
+					n->defnames = $5;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name WITH definition
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = $7;
+					$$ = (Node *) n;
+				}
 		;
 
 definition: '(' def_list ')'						{ $$ = $2; }
@@ -6269,6 +6296,10 @@ def_elem:	ColLabel '=' def_arg
 				}
 		;
 
+list_of_definitions: definition						{ $$ = list_make1($1); }
+			| list_of_definitions ',' definition	{ $$ = lappend($1, $3); }
+		;
+
 /* Note: any simple identifier will be returned as a type name! */
 def_arg:	func_type						{ $$ = (Node *) $1; }
 			| reserved_keyword				{ $$ = (Node *) makeString(pstrdup($1)); }
@@ -6804,6 +6835,8 @@ object_type_name:
 
 drop_type_name:
 			ACCESS METHOD							{ $$ = OBJECT_ACCESS_METHOD; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| EVENT TRIGGER							{ $$ = OBJECT_EVENT_TRIGGER; }
 			| EXTENSION								{ $$ = OBJECT_EXTENSION; }
 			| FOREIGN DATA_P WRAPPER				{ $$ = OBJECT_FDW; }
@@ -9120,6 +9153,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) makeString($5);
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) makeString($5);
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -10128,6 +10181,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) makeString($5);
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) makeString($5);
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11284,6 +11355,34 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY name ADD_P VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = false;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN ENCRYPTION KEY name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16719,6 +16818,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16782,6 +16882,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17264,6 +17365,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17352,6 +17454,7 @@ bare_label_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index f668abfcb336..d0bec0a06d19 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));
+		{
+			*parstate->paramTypes = repalloc_array(*parstate->paramTypes,
+												   Oid, paramno);
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc_array(*parstate->paramOrigTbls,
+														  Oid, paramno);
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc_array(*parstate->paramOrigCols,
+														  AttrNumber, paramno);
+		}
 		else
-			*parstate->paramTypes = (Oid *) palloc(paramno * sizeof(Oid));
+		{
+			*parstate->paramTypes = palloc_array(Oid, paramno);
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc_array(Oid, paramno);
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc_array(AttrNumber, paramno);
+		}
 		/* 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 bd8057bc3e74..37406dd73d1f 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -594,6 +594,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/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 383bc4776ef9..512bc92b2964 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2254,12 +2254,27 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 									valptr),
 							 errhint("Valid values are: \"false\", 0, \"true\", 1, \"database\".")));
 			}
+			else if (strcmp(nameptr, "_pq_.column_encryption") == 0)
+			{
+				/*
+				 * Right now, the only accepted value is "1".  This gives room
+				 * to expand this into a version number, for example.
+				 */
+				if (strcmp(valptr, "1") == 0)
+					port->column_encryption_enabled = true;
+				else
+					ereport(FATAL,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for parameter \"%s\": \"%s\"",
+									"column_encryption",
+									valptr),
+							 errhint("Valid values are: 1.")));
+			}
 			else if (strncmp(nameptr, "_pq_.", 5) == 0)
 			{
 				/*
 				 * Any option beginning with _pq_. is reserved for use as a
-				 * protocol-level option, but at present no such options are
-				 * defined.
+				 * protocol-level option.
 				 */
 				unrecognized_protocol_options =
 					lappend(unrecognized_protocol_options, pstrdup(nameptr));
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 5352d5f4c6bb..af9e0e39e775 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,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"
 
@@ -678,6 +679,8 @@ pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 								 const char *query_string,
 								 Oid **paramTypes,
 								 int *numParams,
+								 Oid **paramOrigTbls,
+								 AttrNumber **paramOrigCols,
 								 QueryEnvironment *queryEnv)
 {
 	Query	   *query;
@@ -692,7 +695,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.
@@ -1366,6 +1369,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_array(Oid, numParams);
+	AttrNumber *paramOrigCols = palloc_array(AttrNumber, numParams);
 
 	/*
 	 * Report query to various monitoring facilities.
@@ -1486,6 +1491,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 														  query_string,
 														  &paramTypes,
 														  &numParams,
+														  &paramOrigTbls,
+														  &paramOrigCols,
 														  NULL);
 
 		/* Done with the snapshot used for parsing */
@@ -1516,6 +1523,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 */
@@ -1813,6 +1822,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (type_is_encrypted(ptype))
+			{
+				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;
@@ -2603,8 +2622,44 @@ 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 (MyProcPort->column_encryption_enabled && type_is_encrypted(ptype))
+		{
+			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);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			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/tcop/utility.c b/src/backend/tcop/utility.c
index aa0081578788..549d31e5d4bf 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -30,6 +30,7 @@
 #include "commands/alter.h"
 #include "commands/async.h"
 #include "commands/cluster.h"
+#include "commands/colenccmds.h"
 #include "commands/collationcmds.h"
 #include "commands/comment.h"
 #include "commands/conversioncmds.h"
@@ -137,6 +138,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1443,14 @@ ProcessUtilitySlow(ParseState *pstate,
 															stmt->definition,
 															&secondaryObject);
 							break;
+						case OBJECT_CEK:
+							Assert(stmt->args == NIL);
+							address = CreateCEK(pstate, stmt);
+							break;
+						case OBJECT_CMK:
+							Assert(stmt->args == NIL);
+							address = CreateCMK(pstate, stmt);
+							break;
 						case OBJECT_COLLATION:
 							Assert(stmt->args == NIL);
 							address = DefineCollation(pstate,
@@ -1903,6 +1913,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2239,12 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_COLUMN:
 			tag = CMDTAG_ALTER_TABLE;
 			break;
+		case OBJECT_CEK:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+		case OBJECT_CMK:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
 		case OBJECT_CONVERSION:
 			tag = CMDTAG_ALTER_CONVERSION;
 			break;
@@ -2640,6 +2660,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_DROP_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_DROP_COLUMN_MASTER_KEY;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2760,6 +2786,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_COLLATION:
 					tag = CMDTAG_CREATE_COLLATION;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_CREATE_COLUMN_MASTER_KEY;
+					break;
 				case OBJECT_ACCESS_METHOD:
 					tag = CMDTAG_CREATE_ACCESS_METHOD;
 					break;
@@ -3063,6 +3095,10 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3217,7 +3253,7 @@ CreateCommandTag(Node *parsetree)
 			break;
 
 		default:
-			elog(WARNING, "unrecognized node type: %d",
+			elog(ERROR, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
 			tag = CMDTAG_UNKNOWN;
 			break;
@@ -3688,6 +3724,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/arrayfuncs.c b/src/backend/utils/adt/arrayfuncs.c
index 495e449a9e9a..405f08a9b378 100644
--- a/src/backend/utils/adt/arrayfuncs.c
+++ b/src/backend/utils/adt/arrayfuncs.c
@@ -3386,6 +3386,7 @@ construct_array_builtin(Datum *elems, int nelems, Oid elmtype)
 			break;
 
 		case OIDOID:
+		case REGCLASSOID:
 		case REGTYPEOID:
 			elmlen = sizeof(Oid);
 			elmbyval = true;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index a16a63f4957b..d5ea1689e29d 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -24,7 +24,10 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_namespace.h"
@@ -2658,6 +2661,25 @@ type_is_multirange(Oid typid)
 	return (get_typtype(typid) == TYPTYPE_MULTIRANGE);
 }
 
+bool
+type_is_encrypted(Oid typid)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+		bool		result;
+
+		result = (typtup->typcategory == TYPCATEGORY_ENCRYPTED);
+		ReleaseSysCache(tp);
+		return result;
+	}
+	else
+		return false;
+}
+
 /*
  * get_type_category_preferred
  *
@@ -3679,3 +3701,92 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+Oid
+get_cek_oid(const char *cekname, bool missing_ok)
+{
+	Oid			oid;
+
+	oid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid,
+						  CStringGetDatum(cekname));
+	if (!OidIsValid(oid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist", cekname)));
+	return oid;
+}
+
+char *
+get_cek_name(Oid cekid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cekname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+		return NULL;
+	}
+
+	cekname = pstrdup(NameStr(((Form_pg_colenckey) GETSTRUCT(tup))->cekname));
+
+	ReleaseSysCache(tup);
+
+	return cekname;
+}
+
+Oid
+get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok)
+{
+	Oid			cekdataid;
+
+	cekdataid = GetSysCacheOid2(CEKDATACEKCMK, Anum_pg_colenckeydata_oid,
+								ObjectIdGetDatum(cekid),
+								ObjectIdGetDatum(cmkid));
+	if (!OidIsValid(cekdataid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" has no data for master key \"%s\"",
+						get_cek_name(cekid, false), get_cmk_name(cmkid, false))));
+
+	return cekdataid;
+}
+
+Oid
+get_cmk_oid(const char *cmkname, bool missing_ok)
+{
+	Oid			oid;
+
+	oid = GetSysCacheOid1(CMKNAME, Anum_pg_colmasterkey_oid,
+						  CStringGetDatum(cmkname));
+	if (!OidIsValid(oid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist", cmkname)));
+	return oid;
+}
+
+char *
+get_cmk_name(Oid cmkid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cmkname;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+		return NULL;
+	}
+
+	cmkname = pstrdup(NameStr(((Form_pg_colmasterkey) GETSTRUCT(tup))->cmkname));
+
+	ReleaseSysCache(tup);
+
+	return cmkname;
+}
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 0d6a29567487..f2a5e029e3a6 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,
@@ -417,8 +419,16 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 
 	if (num_params > 0)
 	{
-		plansource->param_types = (Oid *) palloc(num_params * sizeof(Oid));
+		plansource->param_types = palloc_array(Oid, num_params);
 		memcpy(plansource->param_types, param_types, num_params * sizeof(Oid));
+
+		plansource->param_origtbls = palloc0_array(Oid, num_params);
+		if (param_origtbls)
+			memcpy(plansource->param_origtbls, param_origtbls, num_params * sizeof(Oid));
+
+		plansource->param_origcols = palloc0_array(AttrNumber, num_params);
+		if (param_origcols)
+			memcpy(plansource->param_origcols, param_origcols, num_params * sizeof(AttrNumber));
 	}
 	else
 		plansource->param_types = NULL;
@@ -1535,13 +1545,22 @@ CopyCachedPlan(CachedPlanSource *plansource)
 	newsource->commandTag = plansource->commandTag;
 	if (plansource->num_params > 0)
 	{
-		newsource->param_types = (Oid *)
-			palloc(plansource->num_params * sizeof(Oid));
+		newsource->param_types = palloc_array(Oid, plansource->num_params);
 		memcpy(newsource->param_types, plansource->param_types,
 			   plansource->num_params * sizeof(Oid));
+		if (plansource->param_origtbls)
+		{
+			newsource->param_origtbls = palloc_array(Oid, plansource->num_params);
+			memcpy(newsource->param_origtbls, plansource->param_origtbls,
+				   plansource->num_params * sizeof(Oid));
+		}
+		if (plansource->param_origcols)
+		{
+			newsource->param_origcols = palloc_array(AttrNumber, plansource->num_params);
+			memcpy(newsource->param_origcols, plansource->param_origcols,
+				   plansource->num_params * sizeof(AttrNumber));
+		}
 	}
-	else
-		newsource->param_types = NULL;
 	newsource->num_params = plansource->num_params;
 	newsource->parserSetup = plansource->parserSetup;
 	newsource->parserSetupArg = plansource->parserSetupArg;
@@ -1549,8 +1568,6 @@ CopyCachedPlan(CachedPlanSource *plansource)
 	newsource->fixed_result = plansource->fixed_result;
 	if (plansource->resultDesc)
 		newsource->resultDesc = CreateTupleDescCopy(plansource->resultDesc);
-	else
-		newsource->resultDesc = NULL;
 	newsource->context = source_context;
 
 	querytree_context = AllocSetContextCreate(source_context,
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index eec644ec8489..6337f27bfcfd 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.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 +270,43 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		256
 	},
+	{
+		ColumnEncKeyDataRelationId, /* CEKDATACEKCMK */
+		ColumnEncKeyCekidCmkidIndexId,
+		2,
+		{
+			Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid,
+		},
+		8
+	},
+	{
+		ColumnEncKeyDataRelationId, /* CEKDATAOID */
+		ColumnEncKeyDataOidIndexId,
+		1,
+		{
+			Anum_pg_colenckeydata_oid,
+		},
+		8
+	},
+	{
+		ColumnEncKeyRelationId, /* CEKNAME */
+		ColumnEncKeyNameIndexId,
+		1,
+		{
+			Anum_pg_colenckey_cekname,
+		},
+		8
+	},
+	{
+		ColumnEncKeyRelationId, /* CEKOID */
+		ColumnEncKeyOidIndexId,
+		1,
+		{
+			Anum_pg_colenckey_oid,
+		},
+		8
+	},
 	{OperatorClassRelationId,	/* CLAAMNAMENSP */
 		OpclassAmNameNspIndexId,
 		3,
@@ -289,6 +329,24 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
+	{
+		ColumnMasterKeyRelationId, /* CMKNAME */
+		ColumnMasterKeyNameIndexId,
+		1,
+		{
+			Anum_pg_colmasterkey_cmkname,
+		},
+		8
+	},
+	{
+		ColumnMasterKeyRelationId,	/* CMKOID */
+		ColumnMasterKeyOidIndexId,
+		1,
+		{
+			Anum_pg_colmasterkey_oid,
+		},
+		8
+	},
 	{CollationRelationId,		/* COLLNAMEENCNSP */
 		CollationNameEncNspIndexId,
 		3,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 0543c574c67e..de837ec3cddd 100644
--- a/src/backend/utils/mb/mbutils.c
+++ b/src/backend/utils/mb/mbutils.c
@@ -36,7 +36,9 @@
 
 #include "access/xact.h"
 #include "catalog/namespace.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
@@ -129,6 +131,12 @@ PrepareClientEncoding(int encoding)
 		encoding == PG_SQL_ASCII)
 		return 0;
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	if (IsTransactionState())
 	{
 		/*
@@ -236,6 +244,12 @@ SetClientEncoding(int encoding)
 		return 0;
 	}
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	/*
 	 * Search the cache for the entry previously prepared by
 	 * PrepareClientEncoding; if there isn't one, we lose.  While at it,
@@ -296,7 +310,9 @@ InitializeClientEncoding(void)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("conversion between %s and %s is not supported",
 						pg_enc2name_tbl[pending_client_encoding].name,
-						GetDatabaseEncodingName())));
+						GetDatabaseEncodingName()),
+				 (MyProcPort->column_encryption_enabled) ?
+				 errdetail("Encoding conversion is not possible when column encryption is enabled.") : 0));
 	}
 
 	/*
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 44fa52cc55fe..89feceb3bde1 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -201,6 +201,12 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading user-defined collations");
 	(void) getCollations(fout, &numCollations);
 
+	pg_log_info("reading column master keys");
+	getColumnMasterKeys(fout);
+
+	pg_log_info("reading column encryption keys");
+	getColumnEncryptionKeys(fout);
+
 	pg_log_info("reading user-defined conversions");
 	getConversions(fout, &numConversions);
 
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index e8b78982971e..f89daaa23171 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -84,6 +84,7 @@ typedef struct _connParams
 	char	   *pghost;
 	char	   *username;
 	trivalue	promptPassword;
+	int			column_encryption;
 	/* If not NULL, this overrides the dbname obtained from command line */
 	/* (but *only* the DB name, not anything else in the connstring) */
 	char	   *override_dbname;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 233198afc0c9..8a2bd7fb95ab 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3411,6 +3411,8 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te)
 
 	/* objects that don't require special decoration */
 	if (strcmp(type, "COLLATION") == 0 ||
+		strcmp(type, "COLUMN ENCRYPTION KEY") == 0 ||
+		strcmp(type, "COLUMN MASTER KEY") == 0 ||
 		strcmp(type, "CONVERSION") == 0 ||
 		strcmp(type, "DOMAIN") == 0 ||
 		strcmp(type, "TABLE") == 0 ||
@@ -3588,6 +3590,8 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 		if (strcmp(te->desc, "AGGREGATE") == 0 ||
 			strcmp(te->desc, "BLOB") == 0 ||
 			strcmp(te->desc, "COLLATION") == 0 ||
+			strcmp(te->desc, "COLUMN ENCRYPTION KEY") == 0 ||
+			strcmp(te->desc, "COLUMN MASTER KEY") == 0 ||
 			strcmp(te->desc, "CONVERSION") == 0 ||
 			strcmp(te->desc, "DATABASE") == 0 ||
 			strcmp(te->desc, "DOMAIN") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index 28baa68fd4e9..960c3f39a91c 100644
--- a/src/bin/pg_dump/pg_backup_db.c
+++ b/src/bin/pg_dump/pg_backup_db.c
@@ -133,8 +133,8 @@ ConnectDatabase(Archive *AHX,
 	 */
 	do
 	{
-		const char *keywords[8];
-		const char *values[8];
+		const char *keywords[9];
+		const char *values[9];
 		int			i = 0;
 
 		/*
@@ -159,6 +159,11 @@ ConnectDatabase(Archive *AHX,
 		}
 		keywords[i] = "fallback_application_name";
 		values[i++] = progname;
+		if (cparams->column_encryption)
+		{
+			keywords[i] = "column_encryption";
+			values[i++] = "1";
+		}
 		keywords[i] = NULL;
 		values[i++] = NULL;
 		Assert(i <= lengthof(keywords));
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f8c4cb8d183d..478872801f1e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_authid_d.h"
 #include "catalog/pg_cast_d.h"
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_default_acl_d.h"
 #include "catalog/pg_largeobject_d.h"
 #include "catalog/pg_largeobject_metadata_d.h"
@@ -225,6 +226,8 @@ static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
 static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
 static void dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo);
 static void dumpCollation(Archive *fout, const CollInfo *collinfo);
+static void dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo);
+static void dumpColumnMasterKey(Archive *fout, const CmkInfo *cekinfo);
 static void dumpConversion(Archive *fout, const ConvInfo *convinfo);
 static void dumpRule(Archive *fout, const RuleInfo *rinfo);
 static void dumpAgg(Archive *fout, const AggInfo *agginfo);
@@ -384,6 +387,7 @@ main(int argc, char **argv)
 		{"attribute-inserts", no_argument, &dopt.column_inserts, 1},
 		{"binary-upgrade", no_argument, &dopt.binary_upgrade, 1},
 		{"column-inserts", no_argument, &dopt.column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &dopt.cparams.column_encryption, 1},
 		{"disable-dollar-quoting", no_argument, &dopt.disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &dopt.disable_triggers, 1},
 		{"enable-row-security", no_argument, &dopt.enable_row_security, 1},
@@ -676,6 +680,9 @@ main(int argc, char **argv)
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
 	 */
+	if (dopt.cparams.column_encryption && dopt.dump_inserts == 0)
+		pg_fatal("option --decrypt_encrypted_columns requires option --inserts, --rows-per-insert, or --column-inserts");
+
 	if (dopt.do_nothing && dopt.dump_inserts == 0)
 		pg_fatal("option --on-conflict-do-nothing requires option --inserts, --rows-per-insert, or --column-inserts");
 
@@ -1021,6 +1028,7 @@ help(const char *progname)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security (dump only content user has\n"
@@ -5517,6 +5525,138 @@ getCollations(Archive *fout, int *numCollations)
 	return collinfo;
 }
 
+/*
+ * getColumnEncryptionKeys
+ *	  get information about column encryption keys
+ */
+void
+getColumnEncryptionKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CekInfo	   *cekinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cekname;
+	int			i_cekowner;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname,\n"
+					  " cek.cekowner\n"
+					  "FROM pg_colenckey cek");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cekname = PQfnumber(res, "cekname");
+	i_cekowner = PQfnumber(res, "cekowner");
+
+	cekinfo = pg_malloc(ntups * sizeof(CekInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		PGresult	   *res2;
+		int				ntups2;
+
+		cekinfo[i].dobj.objType = DO_CEK;
+		cekinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cekinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cekinfo[i].dobj);
+		cekinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cekname));
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT (SELECT cmkname FROM pg_catalog.pg_colmasterkey WHERE pg_colmasterkey.oid = ckdcmkid) AS cekcmkname,\n"
+						  " ckdcmkalg, ckdencval\n"
+						  "FROM pg_catalog.pg_colenckeydata\n"
+						  "WHERE ckdcekid = %u", cekinfo[i].dobj.catId.oid);
+		res2 = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		ntups2 = PQntuples(res2);
+		cekinfo[i].numdata = ntups2;
+		cekinfo[i].cekcmknames = pg_malloc(sizeof(char *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			cekinfo[i].cekcmknames[j] = pg_strdup(PQgetvalue(res2, j, PQfnumber(res2, "cekcmkname")));
+			cekinfo[i].cekcmkalgs[j] = atoi(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkalg")));
+			cekinfo[i].cekencvals[j] = pg_strdup(PQgetvalue(res2, j, PQfnumber(res2, "ckdencval")));
+		}
+		PQclear(res2);
+
+		selectDumpableObject(&(cekinfo[i].dobj), fout);
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * getColumnMasterKeys
+ *	  get information about column master keys
+ */
+void
+getColumnMasterKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CmkInfo	   *cmkinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cmkname;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname,\n"
+					  " cmk.cmkowner, cmk.cmkrealm\n"
+					  "FROM pg_colmasterkey cmk");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cmkname = PQfnumber(res, "cmkname");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+
+	cmkinfo = pg_malloc(ntups * sizeof(CmkInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		cmkinfo[i].dobj.objType = DO_CMK;
+		cmkinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cmkinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cmkinfo[i].dobj);
+		cmkinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cmkname));
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8107,6 +8247,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcekname;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8216,17 +8359,29 @@ 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");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBuffer(q,
+						  "(SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek) AS attcekname,\n"
+						  "CASE atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcekname,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\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);
@@ -8245,6 +8400,9 @@ 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_attencdet = PQfnumber(res, "attencdet");
+	i_attencalg = PQfnumber(res, "attencalg");
 	i_attisdropped = PQfnumber(res, "attisdropped");
 	i_attlen = PQfnumber(res, "attlen");
 	i_attalign = PQfnumber(res, "attalign");
@@ -8306,6 +8464,9 @@ 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->attencdet = (bool *) pg_malloc(numatts * sizeof(bool));
+		tbinfo->attencalg = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char));
@@ -8334,6 +8495,18 @@ 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;
+			if (!PQgetisnull(res, r, i_attencdet))
+				tbinfo->attencdet[j] = (PQgetvalue(res, r, i_attencdet)[0] == 't');
+			else
+				tbinfo->attencdet[j] = 0;
+			if (!PQgetisnull(res, r, i_attencalg))
+				tbinfo->attencalg[j] = atoi(PQgetvalue(res, r, i_attencalg));
+			else
+				tbinfo->attencalg[j] = 0;
 			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));
@@ -9858,6 +10031,12 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_OPFAMILY:
 			dumpOpfamily(fout, (const OpfamilyInfo *) dobj);
 			break;
+		case DO_CEK:
+			dumpColumnEncryptionKey(fout, (const CekInfo *) dobj);
+			break;
+		case DO_CMK:
+			dumpColumnMasterKey(fout, (const CmkInfo *) dobj);
+			break;
 		case DO_COLLATION:
 			dumpCollation(fout, (const CollInfo *) dobj);
 			break;
@@ -13251,6 +13430,153 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+static const char *
+get_encalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			return "AEAD_AES_128_CBC_HMAC_SHA_256";
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			return "AEAD_AES_192_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			return "AEAD_AES_256_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			return "AEAD_AES_256_CBC_HMAC_SHA_512";
+
+		default:
+			return NULL;
+	}
+}
+
+/*
+ * dumpColumnEncryptionKey
+ *	  dump the definition of the given column encryption key
+ */
+static void
+dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcekname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcekname = pg_strdup(fmtId(cekinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN ENCRYPTION KEY %s;\n",
+					  qcekname);
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  qcekname);
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtId(cekinfo->cekcmknames[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_encalg_name(cekinfo->cekcmkalgs[i]));
+		appendPQExpBuffer(query, "encrypted_value = ");
+		appendStringLiteralAH(query, cekinfo->cekencvals[i], fout);
+
+		appendPQExpBuffer(query, ")");
+		if (i < cekinfo->numdata - 1)
+		appendPQExpBuffer(query, ", ");
+	}
+
+	appendPQExpBufferStr(query, ";\n");
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cekinfo->dobj.catId, cekinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cekinfo->dobj.name,
+								  .owner = cekinfo->rolname,
+								  .description = "COLUMN ENCRYPTION KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					NULL, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 NULL, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcekname);
+}
+
+/*
+ * dumpColumnMasterKey
+ *	  dump the definition of the given column master key
+ */
+static void
+dumpColumnMasterKey(Archive *fout, const CmkInfo *cmkinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcmkname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcmkname = pg_strdup(fmtId(cmkinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN MASTER KEY %s;\n",
+					  qcmkname);
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  qcmkname);
+
+	appendPQExpBuffer(query, "realm = ");
+	appendStringLiteralAH(query, cmkinfo->cmkrealm, fout);
+
+	appendPQExpBufferStr(query, ");\n");
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cmkinfo->dobj.catId, cmkinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cmkinfo->dobj.name,
+								  .owner = cmkinfo->rolname,
+								  .description = "COLUMN MASTER KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN MASTER KEY", qcmkname,
+					NULL, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 NULL, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15334,6 +15660,22 @@ 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]));
+						/*
+						 * To reduce output size, we don't print the default
+						 * of encryption_type, but we do print the default of
+						 * algorithm, since we might want to change to a new
+						 * default algorithm sometime in the future.
+						 */
+						if (tbinfo->attencdet[j])
+							appendPQExpBuffer(q, "encryption_type = deterministic, ");
+						appendPQExpBuffer(q, "algorithm = '%s')",
+										  get_encalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -17898,6 +18240,8 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_ACCESS_METHOD:
 			case DO_OPCLASS:
 			case DO_OPFAMILY:
+			case DO_CEK:
+			case DO_CMK:
 			case DO_COLLATION:
 			case DO_CONVERSION:
 			case DO_TABLE:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 427f5d45f65b..cbdbae903f33 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -47,6 +47,8 @@ typedef enum
 	DO_ACCESS_METHOD,
 	DO_OPCLASS,
 	DO_OPFAMILY,
+	DO_CEK,
+	DO_CMK,
 	DO_COLLATION,
 	DO_CONVERSION,
 	DO_TABLE,
@@ -333,6 +335,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	char	  **attcekname;
+	int		   *attencalg;
+	bool	   *attencdet;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
 	char	   *attalign;		/* attribute align, used by binary_upgrade */
 	bool	   *attislocal;		/* true if attr has local definition */
@@ -664,6 +669,30 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	char	  **cekcmknames;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -711,6 +740,8 @@ extern AccessMethodInfo *getAccessMethods(Archive *fout, int *numAccessMethods);
 extern OpclassInfo *getOpclasses(Archive *fout, int *numOpclasses);
 extern OpfamilyInfo *getOpfamilies(Archive *fout, int *numOpfamilies);
 extern CollInfo *getCollations(Archive *fout, int *numCollations);
+extern void getColumnEncryptionKeys(Archive *fout);
+extern void getColumnMasterKeys(Archive *fout);
 extern ConvInfo *getConversions(Archive *fout, int *numConversions);
 extern TableInfo *getTables(Archive *fout, int *numTables);
 extern void getOwnedSeqs(Archive *fout, TableInfo tblinfo[], int numTables);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb496..e562a677ed6a 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -69,6 +69,8 @@ enum dbObjectTypePriorities
 	PRIO_TSTEMPLATE,
 	PRIO_TSDICT,
 	PRIO_TSCONFIG,
+	PRIO_CMK,
+	PRIO_CEK,
 	PRIO_FDW,
 	PRIO_FOREIGN_SERVER,
 	PRIO_TABLE,
@@ -111,6 +113,8 @@ static const int dbObjectTypePriority[] =
 	PRIO_ACCESS_METHOD,			/* DO_ACCESS_METHOD */
 	PRIO_OPFAMILY,				/* DO_OPCLASS */
 	PRIO_OPFAMILY,				/* DO_OPFAMILY */
+	PRIO_CEK,					/* DO_CEK */
+	PRIO_CMK,					/* DO_CMK */
 	PRIO_COLLATION,				/* DO_COLLATION */
 	PRIO_CONVERSION,			/* DO_CONVERSION */
 	PRIO_TABLE,					/* DO_TABLE */
@@ -1322,6 +1326,16 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "OPERATOR FAMILY %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_CEK:
+			snprintf(buf, bufsize,
+					 "COLUMN ENCRYPTION KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
+		case DO_CMK:
+			snprintf(buf, bufsize,
+					 "COLUMN MASTER KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_COLLATION:
 			snprintf(buf, bufsize,
 					 "COLLATION %s  (ID %d OID %u)",
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 083012ca39d5..bfe76dd315d4 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -93,6 +93,7 @@ static bool dosync = true;
 
 static int	binary_upgrade = 0;
 static int	column_inserts = 0;
+static int	decrypt_encrypted_columns = 0;
 static int	disable_dollar_quoting = 0;
 static int	disable_triggers = 0;
 static int	if_exists = 0;
@@ -154,6 +155,7 @@ main(int argc, char *argv[])
 		{"attribute-inserts", no_argument, &column_inserts, 1},
 		{"binary-upgrade", no_argument, &binary_upgrade, 1},
 		{"column-inserts", no_argument, &column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &decrypt_encrypted_columns, 1},
 		{"disable-dollar-quoting", no_argument, &disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &disable_triggers, 1},
 		{"exclude-database", required_argument, NULL, 6},
@@ -424,6 +426,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --binary-upgrade");
 	if (column_inserts)
 		appendPQExpBufferStr(pgdumpopts, " --column-inserts");
+	if (decrypt_encrypted_columns)
+		appendPQExpBufferStr(pgdumpopts, " --decrypt-encrypted-columns");
 	if (disable_dollar_quoting)
 		appendPQExpBufferStr(pgdumpopts, " --disable-dollar-quoting");
 	if (disable_triggers)
@@ -649,6 +653,7 @@ help(void)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --exclude-database=PATTERN   exclude databases whose name matches PATTERN\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index a869321cdfc3..c5aca652faeb 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -593,6 +593,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO .+;/m,
+		like   => { %full_runs, section_pre_data => 1, },
+		unlike => { no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, section_pre_data => 1, },
+		unlike => { no_owner => 1, },
+	},
+
 	'ALTER FOREIGN DATA WRAPPER dummy OWNER TO' => {
 		regexp => qr/^ALTER FOREIGN DATA WRAPPER dummy OWNER TO .+;/m,
 		like   => { %full_runs, section_pre_data => 1, },
@@ -1193,6 +1205,24 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1611,6 +1641,24 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3984,8 +4032,12 @@
 			next;
 		}
 
-		# Add terminating semicolon
-		$create_sql{$test_db} .= $tests{$test}->{create_sql} . ";";
+		# Normalize command ending: strip all line endings, add
+		# semicolon if missing, add two newlines.
+		my $create_sql = $tests{$test}->{create_sql};
+		chomp $create_sql;
+		$create_sql .= ';' unless substr($create_sql, -1) eq ';';
+		$create_sql{$test_db} .= $create_sql . "\n\n";
 	}
 }
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index a141146e706b..2306f4ca60b9 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)
@@ -1492,6 +1495,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 e611e3266d0c..d6846acf5f74 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,36 @@ 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 c645d66418a9..3fbfe5dc6865 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1532,7 +1532,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;
@@ -1548,6 +1548,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;
@@ -1846,7 +1847,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)
@@ -1860,7 +1861,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");
@@ -1911,6 +1912,15 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!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 ||
@@ -2034,6 +2044,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)
@@ -2126,6 +2138,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 f8ce1a07060d..9f4fce26fdfe 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 2399cffa3fba..636729df1d28 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 f5b9e268f200..0b812f332211 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 dbe89d7eb2c0..b8ec27df1cc4 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1169,6 +1169,16 @@ static const VersionedQuery Query_for_list_of_subscriptions[] = {
 	{0, NULL}
 };
 
+static const VersionedQuery Query_for_list_of_ceks[] = {
+	{160000, "SELECT cekname FROM pg_catalog.pg_colenckey WHERE cekname LIKE '%s'"},
+	{0, NULL}
+};
+
+static const VersionedQuery Query_for_list_of_cmks[] = {
+	{160000, "SELECT cmkname FROM pg_catalog.pg_colmasterkey WHERE cmkname LIKE '%s'"},
+	{0, NULL}
+};
+
 /*
  * This is a list of all "things" in Pgsql, which can show up after CREATE or
  * DROP; and there is also a query to get a list of them.
@@ -1202,6 +1212,8 @@ static const pgsql_thing_t words_after_create[] = {
 	{"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", NULL, NULL, &Query_for_list_of_collations},
+	{"COLUMN ENCRYPTION KEY", NULL, NULL, NULL},
+	{"COLUMN MASTER KEY KEY", NULL, NULL, NULL},
 
 	/*
 	 * CREATE CONSTRAINT TRIGGER is not supported here because it is designed
@@ -1692,7 +1704,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",
@@ -1900,6 +1912,22 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "COLLATION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "REFRESH VERSION", "RENAME TO", "SET SCHEMA");
 
+	/* ALTER/DROP COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "ENCRYPTION", "KEY"))
+		COMPLETE_WITH_VERSIONED_QUERY(Query_for_list_of_ceks);
+
+	/* ALTER COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("ADD VALUE (", "DROP VALUE (", "OWNER TO", "RENAME TO");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_VERSIONED_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("OWNER TO", "RENAME TO");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2794,6 +2822,26 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("true", "false");
 	}
 
+	/* CREATE/ALTER/DROP COLUMN ... KEY */
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN"))
+		COMPLETE_WITH("ENCRYPTION", "MASTER");
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN", "ENCRYPTION|MASTER"))
+		COMPLETE_WITH("KEY");
+
+	/* CREATE COLUMN ENCRYPTION KEY */
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("VALUES");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH", "VALUES"))
+		COMPLETE_WITH("(");
+
+	/* CREATE COLUMN MASTER KEY */
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("(");
+
 	/* CREATE DATABASE */
 	else if (Matches("CREATE", "DATABASE", MatchAny))
 		COMPLETE_WITH("OWNER", "TEMPLATE", "ENCODING", "TABLESPACE",
@@ -3517,6 +3565,7 @@ psql_completion(const char *text, int start, int end)
 			 Matches("DROP", "ACCESS", "METHOD", MatchAny) ||
 			 (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
 			  ends_with(prev_wd, ')')) ||
+			 Matches("DROP", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
 			 Matches("DROP", "EVENT", "TRIGGER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "TABLE", MatchAny) ||
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 971a74cf22b4..db1f3b8811a1 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/dependency.h b/src/include/catalog/dependency.h
index 98a1a8428907..1a2a8177d1a7 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -92,6 +92,9 @@ typedef enum ObjectClass
 	OCLASS_TYPE,				/* pg_type */
 	OCLASS_CAST,				/* pg_cast */
 	OCLASS_COLLATION,			/* pg_collation */
+	OCLASS_CEK,					/* pg_colenckey */
+	OCLASS_CEKDATA,				/* pg_colenckeydata */
+	OCLASS_CMK,					/* pg_colmasterkey */
 	OCLASS_CONSTRAINT,			/* pg_constraint */
 	OCLASS_CONVERSION,			/* pg_conversion */
 	OCLASS_DEFAULT,				/* pg_attrdef */
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 45ffa99692e9..f3a0b1cee9c0 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -63,6 +63,9 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_colmasterkey.h',
+  'pg_colenckey.h',
+  'pg_colenckeydata.h',
 ]
 
 bki_data = [
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 61cd5914304a..a0bd7081327c 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 4cc129bebd83..dddb27113fa1 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 053294c99f37..550a53649beb 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 4471eb6bbea4..878b0bcbbf50 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 000000000000..095ed712b03b
--- /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 000000000000..3e4dea72180c
--- /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 000000000000..0344cc420168
--- /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 dbcae7ffdd21..0ca401ffe478 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 bc5f8213f3af..4737a7f9ed17 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 b3b6a7e616a9..5343580b0632 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 a07e737a337e..7f6298452fb2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8033,9 +8033,9 @@
   proname => 'pg_prepared_statement', prorows => '1000', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'record',
   proargtypes => '',
-  proallargtypes => '{text,text,timestamptz,_regtype,_regtype,bool,int8,int8}',
-  proargmodes => '{o,o,o,o,o,o,o,o}',
-  proargnames => '{name,statement,prepare_time,parameter_types,result_types,from_sql,generic_plans,custom_plans}',
+  proallargtypes => '{text,text,timestamptz,_regtype,_regclass,_int2,_regtype,bool,int8,int8}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{name,statement,prepare_time,parameter_types,parameter_orig_tables,parameter_orig_columns,result_types,from_sql,generic_plans,custom_plans}',
   prosrc => 'pg_prepared_statement' },
 { oid => '2511', descr => 'get the open cursors for this session',
   proname => 'pg_cursor', prorows => '1000', proretset => 't',
@@ -11820,4 +11820,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 df4587946357..851063cf5f05 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 => 'b',
+  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 => 'b',
+  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 48a255913742..c1d30903b5b2 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -294,6 +294,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPCATEGORY_USER		'U'
 #define  TYPCATEGORY_BITSTRING	'V' /* er ... "varbit"? */
 #define  TYPCATEGORY_UNKNOWN	'X'
+#define  TYPCATEGORY_ENCRYPTED	'Y'
 #define  TYPCATEGORY_INTERNAL	'Z'
 
 #define  TYPALIGN_CHAR			'c' /* char alignment (i.e. unaligned) */
diff --git a/src/include/commands/colenccmds.h b/src/include/commands/colenccmds.h
new file mode 100644
index 000000000000..d9741e100bb2
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,25 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/colenccmds.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COLENCCMDS_H
+#define COLENCCMDS_H
+
+#include "catalog/objectaddress.h"
+#include "parser/parse_node.h"
+
+extern ObjectAddress CreateCEK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt);
+extern ObjectAddress CreateCMK(ParseState *pstate, DefineStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 6d452ec6d956..e32c7779c955 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -164,6 +164,7 @@ typedef struct Port
 	 */
 	char	   *database_name;
 	char	   *user_name;
+	bool		column_encryption_enabled;
 	char	   *cmdline_options;
 	List	   *guc_options;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index aead2afd6ef7..8bd834b21bba 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -686,6 +686,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? */
@@ -1864,6 +1865,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2056,6 +2060,19 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	char	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index 3d3a5918c2fc..6c2866d19f8a 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/kwlist.h b/src/include/parser/kwlist.h
index 9a7cc0c6bd1d..813cf3d23d31 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -149,6 +149,7 @@ PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("encryption", ENCRYPTION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -250,6 +251,7 @@ PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("master", MASTER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 962ebf65de18..f6a7178766ef 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 df1ee660d831..da202b28a7c3 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/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f43..d82a2e1171ba 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -29,6 +29,8 @@ PG_CMDTAG(CMDTAG_ALTER_ACCESS_METHOD, "ALTER ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_AGGREGATE, "ALTER AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CAST, "ALTER CAST", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_COLLATION, "ALTER COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY, "ALTER COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_MASTER_KEY, "ALTER COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONSTRAINT, "ALTER CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONVERSION, "ALTER CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_DATABASE, "ALTER DATABASE", false, false, false)
@@ -86,6 +88,8 @@ PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, fals
 PG_CMDTAG(CMDTAG_CREATE_AGGREGATE, "CREATE AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CAST, "CREATE CAST", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_COLLATION, "CREATE COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY, "CREATE COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_MASTER_KEY, "CREATE COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONSTRAINT, "CREATE CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONVERSION, "CREATE CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_DATABASE, "CREATE DATABASE", false, false, false)
@@ -138,6 +142,8 @@ PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CAST, "DROP CAST", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_COLLATION, "DROP COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_ENCRYPTION_KEY, "DROP COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_MASTER_KEY, "DROP COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONSTRAINT, "DROP CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONVERSION, "DROP CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_DATABASE, "DROP DATABASE", false, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 5d34978f329e..7502d71b0d20 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/acl.h b/src/include/utils/acl.h
index 9a4df3a5dacc..a7c51dc3c89a 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -318,6 +318,8 @@ extern bool pg_opclass_ownercheck(Oid opc_oid, Oid roleid);
 extern bool pg_opfamily_ownercheck(Oid opf_oid, Oid roleid);
 extern bool pg_database_ownercheck(Oid db_oid, Oid roleid);
 extern bool pg_collation_ownercheck(Oid coll_oid, Oid roleid);
+extern bool pg_column_encryption_key_ownercheck(Oid cek_oid, Oid roleid);
+extern bool pg_column_master_key_ownercheck(Oid cmk_oid, Oid roleid);
 extern bool pg_conversion_ownercheck(Oid conv_oid, Oid roleid);
 extern bool pg_ts_dict_ownercheck(Oid dict_oid, Oid roleid);
 extern bool pg_ts_config_ownercheck(Oid cfg_oid, Oid roleid);
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 50f028830525..7258d4007705 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -163,6 +163,7 @@ extern bool type_is_rowtype(Oid typid);
 extern bool type_is_enum(Oid typid);
 extern bool type_is_range(Oid typid);
 extern bool type_is_multirange(Oid typid);
+extern bool type_is_encrypted(Oid typid);
 extern void get_type_category_preferred(Oid typid,
 										char *typcategory,
 										bool *typispreferred);
@@ -202,6 +203,11 @@ extern Oid	get_publication_oid(const char *pubname, bool missing_ok);
 extern char *get_publication_name(Oid pubid, bool missing_ok);
 extern Oid	get_subscription_oid(const char *subname, bool missing_ok);
 extern char *get_subscription_name(Oid subid, bool missing_ok);
+extern Oid	get_cek_oid(const char *cekname, bool missing_ok);
+extern char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok);
+extern Oid	get_cmk_oid(const char *cmkname, bool missing_ok);
+extern char *get_cmk_name(Oid cmkid, bool missing_ok);
 
 #define type_is_array(typid)  (get_element_type(typid) != InvalidOid)
 /* type_is_array_domain accepts both plain arrays and domains over arrays */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 0499635f5945..9a7cf794cecf 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -101,6 +101,10 @@ 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 +203,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 4463ea66bea5..489c6ee734bb 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAME,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAME,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 1d31b256fc9e..4812f862ca6a 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 e8bcc8837091..fabad38ac0f1 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 746e9b4f1efc..f2a37390847e 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -341,6 +341,14 @@ 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)},
+
+	{"column_encryption", "PGCOLUMNENCRYPTION", "0", NULL,
+		"Column-Encryption", "", 1,
+	offsetof(struct pg_conn, column_encryption_setting)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -2454,6 +2462,7 @@ PQconnectPoll(PGconn *conn)
 #ifdef ENABLE_GSS
 		conn->try_gss = (conn->gssencmode[0] != 'd');	/* "disable" */
 #endif
+		conn->column_encryption_enabled = (conn->column_encryption_setting[0] == '1');
 
 		reset_connection_state_machine = false;
 		need_new_connection = true;
@@ -3249,7 +3258,7 @@ PQconnectPoll(PGconn *conn)
 				 * request or an error here.  Anything else probably means
 				 * it's not Postgres on the other end at all.
 				 */
-				if (!(beresp == 'R' || beresp == 'E'))
+				if (!(beresp == 'R' || beresp == 'E' || beresp == 'v'))
 				{
 					appendPQExpBuffer(&conn->errorMessage,
 									  libpq_gettext("expected authentication request from server, but received %c\n"),
@@ -3405,6 +3414,17 @@ PQconnectPoll(PGconn *conn)
 
 					goto error_return;
 				}
+				else if (beresp == 'v')
+				{
+					if (pqGetNegotiateProtocolVersion3(conn))
+					{
+						/* We'll come back when there is more data */
+						return PGRES_POLLING_READING;
+					}
+					/* OK, we read the message; mark data consumed */
+					conn->inStart = conn->inCursor;
+					goto error_return;
+				}
 
 				/* It is an authentication request. */
 				conn->auth_req_received = true;
@@ -4080,6 +4100,22 @@ freePGconn(PGconn *conn)
 	free(conn->krbsrvname);
 	free(conn->gsslib);
 	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 */
 	free(conn->write_err_msg);
 	free(conn->inBuffer);
diff --git a/src/interfaces/libpq/fe-encrypt-openssl.c b/src/interfaces/libpq/fe-encrypt-openssl.c
new file mode 100644
index 000000000000..974a3aa810af
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,766 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 <openssl/evp.h>
+
+
+#ifdef TEST_ENCRYPT
+
+/*
+ * Test data from
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5;
+ */
+
+/*
+ * 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;
+
+	if (encrlen < 0)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("encrypted value has invalid length"));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	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 of 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 of 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;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	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, md_value, &md_len))
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("digest signing failed: %s"),
+						  ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	Assert(md_len == md_hash_length(md));
+	memcpy(iv, md_value, ivlen);
+
+	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 000000000000..b0093dc52704
--- /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 0274c1b156c6..835e3516fff6 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,9 @@
 #include <unistd.h>
 #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);
@@ -1185,6 +1190,382 @@ 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");
+				if (!fp)
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not run command \"%s\": %m\n"), command);
+					free(command);
+					goto fail;
+				}
+				free(command);
+				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
@@ -1253,6 +1634,55 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 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;
@@ -1260,6 +1690,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;
@@ -1531,7 +1962,9 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   NULL,
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1649,6 +2082,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;
@@ -1676,7 +2123,9 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   paramForceColumnEncryptions,
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1777,7 +2226,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;
@@ -1824,14 +2275,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 = &paramDesc->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;
 		}
 	}
@@ -1850,6 +2340,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1868,9 +2359,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
 		{
@@ -2308,12 +2844,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);
 }
@@ -3612,6 +4163,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)
 {
@@ -3797,6 +4359,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 f001137b7692..418433ed6957 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -43,6 +43,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);
@@ -301,6 +303,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 &&
@@ -558,6 +576,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) ||
@@ -572,6 +592,21 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 2, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -593,8 +628,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;
 	}
 
@@ -696,10 +733,31 @@ 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;
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&cekalg, 2, conn))
+				goto not_enough_data;
+			if (pqGetInt(&flags, 2, conn))
+				goto not_enough_data;
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
 		result->paramDescs[i].typid = typid;
+		result->paramDescs[i].cekid = cekid;
+		result->paramDescs[i].cekalg = cekalg;
+		result->paramDescs[i].flags = flags;
 	}
 
 	/* Success! */
@@ -1397,6 +1455,40 @@ reportErrorPosition(PQExpBuffer msg, const char *query, int loc, int encoding)
 }
 
 
+int
+pqGetNegotiateProtocolVersion3(PGconn *conn)
+{
+	int			their_version;
+	int			num;
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+	if (pqGetInt(&their_version, 4, conn) != 0)
+		return EOF;
+	if (pqGetInt(&num, 4, conn) != 0)
+		return EOF;
+	for (int i = 0; i < num; i++)
+	{
+		if (pqGets(&conn->workBuffer, conn))
+			return EOF;
+		if (buf.len > 0)
+			appendPQExpBufferChar(&buf, ' ');
+		appendPQExpBufferStr(&buf, conn->workBuffer.data);
+	}
+
+	if (their_version != conn->pversion)
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("protocol version not supported by server: client uses %d.%d, server supports %d.%d"),
+						  PG_PROTOCOL_MAJOR(conn->pversion), PG_PROTOCOL_MINOR(conn->pversion),
+						  PG_PROTOCOL_MAJOR(their_version), PG_PROTOCOL_MINOR(their_version));
+	else
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("protocol extension not supported by server: %s\n"), buf.data);
+
+	termPQExpBuffer(&buf);
+	return 0;
+}
+
 /*
  * Attempt to read a ParameterStatus message.
  * This is possible in several places, so we break it out as a subroutine.
@@ -1425,6 +1517,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.
@@ -2250,6 +2425,9 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	if (conn->column_encryption_enabled)
+		ADD_STARTUP_OPTION("_pq_.column_encryption", "1");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index 5d68cf2eb3b5..8396888a5b16 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -450,7 +450,8 @@ pqTraceOutputS(FILE *f, const char *message, int *cursor)
 
 /* ParameterDescription */
 static void
-pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -458,12 +459,21 @@ 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);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt16(f, message, cursor);
+			pqTraceOutputInt16(f, message, cursor);
+		}
+	}
 }
 
 /* RowDescription */
 static void
-pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -479,6 +489,11 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
 		pqTraceOutputInt16(f, message, cursor);
 		pqTraceOutputInt32(f, message, cursor, false);
 		pqTraceOutputInt16(f, message, cursor);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt16(f, message, cursor);
+		}
 	}
 }
 
@@ -514,6 +529,30 @@ pqTraceOutputW(FILE *f, const char *message, int *cursor, int length)
 		pqTraceOutputInt16(f, message, cursor);
 }
 
+/* ColumnMasterKey */
+static void
+pqTraceOutputy(FILE *f, const char *message, int *cursor, bool regress)
+{
+	fprintf(f, "ColumnMasterKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+}
+
+/* ColumnEncryptionKey */
+static void
+pqTraceOutputY(FILE *f, const char *message, int *cursor, bool regress)
+{
+	int			len;
+
+	fprintf(f, "ColumnEncryptionKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt16(f, message, cursor);
+	len = pqTraceOutputInt32(f, message, cursor, false);
+	pqTraceOutputNchar(f, len, message, cursor);
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -647,10 +686,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 				fprintf(conn->Pfdebug, "Sync"); /* no message content */
 			break;
 		case 't':				/* Parameter Description */
-			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'T':				/* Row Description */
-			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'v':				/* Negotiate Protocol Version */
 			pqTraceOutputv(conn->Pfdebug, message, &logCursor);
@@ -665,6 +706,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			fprintf(conn->Pfdebug, "Terminate");
 			/* No message content */
 			break;
+		case 'y':
+			pqTraceOutputy(conn->Pfdebug, message, &logCursor, regress);
+			break;
+		case 'Y':
+			pqTraceOutputY(conn->Pfdebug, message, &logCursor, regress);
+			break;
 		case 'Z':				/* Ready For Query */
 			pqTraceOutputZ(conn->Pfdebug, message, &logCursor);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index b7df3224c0f7..0234724396d3 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 c75ed63a2c62..09307dab11c1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -112,6 +112,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -343,6 +346,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.
@@ -396,6 +419,8 @@ 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 */
+	char	   *column_encryption_setting;	/* column_encryption connection parameter (0 or 1) */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -477,6 +502,13 @@ struct pg_conn
 	PGVerbosity verbosity;		/* error/notice message verbosity */
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
+	bool		column_encryption_enabled;	/* parsed version of column_encryption_setting */
+
+	/* 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 */
@@ -673,6 +705,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);
@@ -685,6 +721,7 @@ extern void pqParseInput3(PGconn *conn);
 extern int	pqGetErrorNotice3(PGconn *conn, bool isError);
 extern void pqBuildErrorMessage3(PQExpBuffer msg, const PGresult *res,
 								 PGVerbosity verbosity, PGContextVisibility show_context);
+extern int	pqGetNegotiateProtocolVersion3(PGconn *conn);
 extern int	pqGetCopyData3(PGconn *conn, char **buffer, int async);
 extern int	pqGetline3(PGconn *conn, char *s, int maxlen);
 extern int	pqGetlineAsync3(PGconn *conn, char *buffer, int bufsize);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index bc047e00d620..ab69e982e262 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -23,6 +23,7 @@ endif
 
 if ssl.found()
   libpq_sources += files('fe-secure-common.c')
+  libpq_sources += files('fe-encrypt-openssl.c')
   libpq_sources += files('fe-secure-openssl.c')
 endif
 
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 9256b426c1d4..c45fe86d1d24 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,5 +1,5 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-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 6ba78adb6786..1846594ec516 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 75ac08f943d9..b1ebab90d4e6 100644
--- a/src/interfaces/libpq/test/Makefile
+++ b/src/interfaces/libpq/test/Makefile
@@ -15,10 +15,17 @@ 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)
 
 $(PROGS): $(WIN32RES)
 
+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/src/interfaces/libpq/test/meson.build b/src/interfaces/libpq/test/meson.build
index 16f94c1ed8b1..491ccf30ff9a 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -13,3 +13,5 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+# TODO: libpq_test_encrypt
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874d3..c8ba1705030b 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -24,7 +24,7 @@ ifeq ($(with_ldap),yes)
 SUBDIRS += ldap
 endif
 ifeq ($(with_ssl),openssl)
-SUBDIRS += ssl
+SUBDIRS += column_encryption ssl
 endif
 
 # Test suites that are not safe by default but can be run if selected
@@ -36,7 +36,7 @@ export PG_TEST_EXTRA
 # 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 000000000000..456dbf69d2a4
--- /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 000000000000..7aca84b17a00
--- /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/meson.build b/src/test/column_encryption/meson.build
new file mode 100644
index 000000000000..a0525da36615
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,20 @@
+column_encryption_test_client = executable('test_client',
+  files('test_client.c'),
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+testprep_targets += column_encryption_test_client
+
+tests += {
+  'name': 'column_encryption',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_column_encryption.pl',
+      't/002_cmk_rotation.pl',
+    ],
+  },
+}
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 000000000000..4a23414ee0a7
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,183 @@
+# 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{CREATE COLUMN MASTER KEY ${cmkname} WITH (realm = '')});
+	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{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = ${cmkname}, encrypted_value = '\\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{'PGCOLUMNENCRYPTION'} = '1';
+$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),
+    c smallint 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, c) VALUES (1, $1, $2) \gencr 'val1' 11
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \gencr 'val2' 22
+});
+
+# 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}\t\\\\x[0-9a-f]{96}\n2\t\\\\x[0-9a-f]{96}\t\\\\x[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text
+c|smallint),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result');
+
+{
+	local $ENV{'PGCMKLOOKUP'} = '*=run:broken %k "%b"';
+	$result = $node->psql('postgres', q{SELECT a, b, c 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, c FROM tbl1});
+	is($result,
+		q(1|val1|11
+2|val2|22),
+		'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, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'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, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3|33),
+	'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, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3upd|33),
+	'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 000000000000..ec447872ab24
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,108 @@
+# 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{CREATE COLUMN MASTER KEY ${cmkname} WITH (realm = '')});
+	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{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = cmk1, encrypted_value = '\\x${cekenchex}');});
+
+$ENV{'PGCOLUMNENCRYPTION'} = '1';
+$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{ALTER COLUMN ENCRYPTION KEY ${cekname} ADD VALUE (column_master_key = cmk2, encrypted_value = '\\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{ALTER COLUMN ENCRYPTION KEY ${cekname} DROP VALUE (column_master_key = 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 000000000000..636de88c7851
--- /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", "33"};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					   3, 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", "33"};
+	int			forces[] = {false, true};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, 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, "", 3, 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 000000000000..9664349f1401
--- /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/meson.build b/src/test/meson.build
index 241d9d48aa53..0d39cedeb109 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -7,6 +7,7 @@ subdir('subscription')
 subdir('modules')
 
 if ssl.found()
+  subdir('column_encryption')
   subdir('ssl')
 endif
 
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 000000000000..cbd8cd983369
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,143 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE ROLE regress_enc_user1;
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+CREATE COLUMN MASTER KEY cmk2 WITH (
+    realm = 'test2'
+);
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'test2'
+);
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "cek1" already has data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "fail" does not exist
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\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    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+\d tbl_447f
+              Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_447f
+                                        Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key cek1 data for master key cmk1 depends on column master key cmk1
+column encryption key cek4 data for master key cmk1 depends on column master key cmk1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ERROR:  column master key "cmk3" already exists
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+ERROR:  column master key "cmkx" does not exist
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ERROR:  column encryption key "cek3" already exists
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET ROLE regress_enc_user1;
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+ERROR:  must be owner of column encryption key cek3
+DROP COLUMN MASTER KEY cmk3;  -- fail
+ERROR:  must be owner of column master key cmk3
+RESET ROLE;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+SET ROLE regress_enc_user1;
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET ROLE;
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ERROR:  column encryption key "cek1" has no data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ERROR:  column master key "fail" does not exist
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+ERROR:  attribute "algorithm" must not be specified
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+ERROR:  column encryption key "fail" does not exist
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 3549b63a79c1..3eca6cdb661a 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -37,6 +37,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk WITH (realm = '');
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -105,7 +107,8 @@ BEGIN
 		('text search template'), ('text search configuration'),
 		('policy'), ('user mapping'), ('default acl'), ('transform'),
 		('operator of access method'), ('function of access method'),
-		('publication namespace'), ('publication relation')
+		('publication namespace'), ('publication relation'),
+		('column encryption key'), ('column encryption key data'), ('column master key')
 	LOOP
 		FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
 		LOOP
@@ -325,6 +328,24 @@ WARNING:  error for publication relation,{addr_nsp,zwei},{}: argument list lengt
 WARNING:  error for publication relation,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist
 WARNING:  error for publication relation,{eins,zwei,drei},{}: argument list length must be exactly 1
 WARNING:  error for publication relation,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei"
+WARNING:  error for column encryption key,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{}: name list length must be exactly 1
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: name list length must be exactly 1
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: name list length must be exactly 1
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: name list length must be exactly 1
+WARNING:  error for column encryption key data,{eins},{}: argument list length must be exactly 1
+WARNING:  error for column encryption key data,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{}: name list length must be exactly 1
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: name list length must be exactly 1
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: name list length must be exactly 1
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: name list length must be exactly 1
+WARNING:  error for column master key,{eins},{}: column master key "eins" does not exist
+WARNING:  error for column master key,{eins},{integer}: column master key "eins" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{}: name list length must be exactly 1
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: name list length must be exactly 1
+WARNING:  error for column master key,{eins,zwei,drei},{}: name list length must be exactly 1
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: name list length must be exactly 1
 -- these object types cannot be qualified names
 SELECT pg_get_object_address('language', '{one}', '{}');
 ERROR:  language "one" does not exist
@@ -380,6 +401,14 @@ SELECT pg_get_object_address('subscription', '{one}', '{}');
 ERROR:  subscription "one" does not exist
 SELECT pg_get_object_address('subscription', '{one,two}', '{}');
 ERROR:  name list length must be exactly 1
+SELECT pg_get_object_address('column encryption key', '{one}', '{}');
+ERROR:  column encryption key "one" does not exist
+SELECT pg_get_object_address('column encryption key', '{one,two}', '{}');
+ERROR:  name list length must be exactly 1
+SELECT pg_get_object_address('column master key', '{one}', '{}');
+ERROR:  column master key "one" does not exist
+SELECT pg_get_object_address('column master key', '{one,two}', '{}');
+ERROR:  name list length must be exactly 1
 -- test successful cases
 WITH objects (type, name, args) AS (VALUES
 				('table', '{addr_nsp, gentable}'::text[], '{}'::text[]),
@@ -402,6 +431,9 @@ WITH objects (type, name, args) AS (VALUES
 				('type', '{addr_nsp.genenum}', '{}'),
 				('cast', '{int8}', '{int4}'),
 				('collation', '{default}', '{}'),
+				('column encryption key', '{addr_cek}', '{}'),
+				('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+				('column master key', '{addr_cmk}', '{}'),
 				('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
 				('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
 				('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -447,59 +479,62 @@ SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*,
 			pg_identify_object_as_address(classid, objid, objsubid) ioa(typ,nms,args),
 			pg_get_object_address(typ, nms, ioa.args) as addr2
 	ORDER BY addr1.classid, addr1.objid, addr1.objsubid;
-           type            |   schema   |       name        |                               identity                               | ?column? 
----------------------------+------------+-------------------+----------------------------------------------------------------------+----------
- default acl               |            |                   | for role regress_addr_user in schema public on tables                | t
- default acl               |            |                   | for role regress_addr_user on tables                                 | t
- type                      | pg_catalog | _int4             | integer[]                                                            | t
- type                      | addr_nsp   | gencomptype       | addr_nsp.gencomptype                                                 | t
- type                      | addr_nsp   | genenum           | addr_nsp.genenum                                                     | t
- type                      | addr_nsp   | gendomain         | addr_nsp.gendomain                                                   | t
- function                  | pg_catalog |                   | pg_catalog.pg_identify_object(pg_catalog.oid,pg_catalog.oid,integer) | t
- aggregate                 | addr_nsp   |                   | addr_nsp.genaggr(integer)                                            | t
- procedure                 | addr_nsp   |                   | addr_nsp.proc(integer)                                               | t
- sequence                  | addr_nsp   | gentable_a_seq    | addr_nsp.gentable_a_seq                                              | t
- table                     | addr_nsp   | gentable          | addr_nsp.gentable                                                    | t
- table column              | addr_nsp   | gentable          | addr_nsp.gentable.b                                                  | t
- index                     | addr_nsp   | gentable_pkey     | addr_nsp.gentable_pkey                                               | t
- table                     | addr_nsp   | parttable         | addr_nsp.parttable                                                   | t
- index                     | addr_nsp   | parttable_pkey    | addr_nsp.parttable_pkey                                              | t
- view                      | addr_nsp   | genview           | addr_nsp.genview                                                     | t
- materialized view         | addr_nsp   | genmatview        | addr_nsp.genmatview                                                  | t
- foreign table             | addr_nsp   | genftable         | addr_nsp.genftable                                                   | t
- foreign table column      | addr_nsp   | genftable         | addr_nsp.genftable.a                                                 | t
- role                      |            | regress_addr_user | regress_addr_user                                                    | t
- server                    |            | addr_fserv        | addr_fserv                                                           | t
- user mapping              |            |                   | regress_addr_user on server integer                                  | t
- foreign-data wrapper      |            | addr_fdw          | addr_fdw                                                             | t
- access method             |            | btree             | btree                                                                | t
- operator of access method |            |                   | operator 1 (integer, integer) of pg_catalog.integer_ops USING btree  | t
- function of access method |            |                   | function 2 (integer, integer) of pg_catalog.integer_ops USING btree  | t
- default value             |            |                   | for addr_nsp.gentable.b                                              | t
- cast                      |            |                   | (bigint AS integer)                                                  | t
- table constraint          | addr_nsp   |                   | a_chk on addr_nsp.gentable                                           | t
- domain constraint         | addr_nsp   |                   | domconstr on addr_nsp.gendomain                                      | t
- conversion                | pg_catalog | koi8_r_to_mic     | pg_catalog.koi8_r_to_mic                                             | t
- language                  |            | plpgsql           | plpgsql                                                              | t
- schema                    |            | addr_nsp          | addr_nsp                                                             | t
- operator class            | pg_catalog | int4_ops          | pg_catalog.int4_ops USING btree                                      | t
- operator                  | pg_catalog |                   | pg_catalog.+(integer,integer)                                        | t
- rule                      |            |                   | "_RETURN" on addr_nsp.genview                                        | t
- trigger                   |            |                   | t on addr_nsp.gentable                                               | t
- operator family           | pg_catalog | integer_ops       | pg_catalog.integer_ops USING btree                                   | t
- policy                    |            |                   | genpol on addr_nsp.gentable                                          | t
- statistics object         | addr_nsp   | gentable_stat     | addr_nsp.gentable_stat                                               | t
- collation                 | pg_catalog | "default"         | pg_catalog."default"                                                 | t
- transform                 |            |                   | for integer on language sql                                          | t
- text search dictionary    | addr_nsp   | addr_ts_dict      | addr_nsp.addr_ts_dict                                                | t
- text search parser        | addr_nsp   | addr_ts_prs       | addr_nsp.addr_ts_prs                                                 | t
- text search configuration | addr_nsp   | addr_ts_conf      | addr_nsp.addr_ts_conf                                                | t
- text search template      | addr_nsp   | addr_ts_temp      | addr_nsp.addr_ts_temp                                                | t
- subscription              |            | regress_addr_sub  | regress_addr_sub                                                     | t
- publication               |            | addr_pub          | addr_pub                                                             | t
- publication relation      |            |                   | addr_nsp.gentable in publication addr_pub                            | t
- publication namespace     |            |                   | addr_nsp in publication addr_pub_schema                              | t
-(50 rows)
+            type            |   schema   |       name        |                               identity                               | ?column? 
+----------------------------+------------+-------------------+----------------------------------------------------------------------+----------
+ default acl                |            |                   | for role regress_addr_user in schema public on tables                | t
+ default acl                |            |                   | for role regress_addr_user on tables                                 | t
+ type                       | pg_catalog | _int4             | integer[]                                                            | t
+ type                       | addr_nsp   | gencomptype       | addr_nsp.gencomptype                                                 | t
+ type                       | addr_nsp   | genenum           | addr_nsp.genenum                                                     | t
+ type                       | addr_nsp   | gendomain         | addr_nsp.gendomain                                                   | t
+ function                   | pg_catalog |                   | pg_catalog.pg_identify_object(pg_catalog.oid,pg_catalog.oid,integer) | t
+ aggregate                  | addr_nsp   |                   | addr_nsp.genaggr(integer)                                            | t
+ procedure                  | addr_nsp   |                   | addr_nsp.proc(integer)                                               | t
+ sequence                   | addr_nsp   | gentable_a_seq    | addr_nsp.gentable_a_seq                                              | t
+ table                      | addr_nsp   | gentable          | addr_nsp.gentable                                                    | t
+ table column               | addr_nsp   | gentable          | addr_nsp.gentable.b                                                  | t
+ index                      | addr_nsp   | gentable_pkey     | addr_nsp.gentable_pkey                                               | t
+ table                      | addr_nsp   | parttable         | addr_nsp.parttable                                                   | t
+ index                      | addr_nsp   | parttable_pkey    | addr_nsp.parttable_pkey                                              | t
+ view                       | addr_nsp   | genview           | addr_nsp.genview                                                     | t
+ materialized view          | addr_nsp   | genmatview        | addr_nsp.genmatview                                                  | t
+ foreign table              | addr_nsp   | genftable         | addr_nsp.genftable                                                   | t
+ foreign table column       | addr_nsp   | genftable         | addr_nsp.genftable.a                                                 | t
+ role                       |            | regress_addr_user | regress_addr_user                                                    | t
+ server                     |            | addr_fserv        | addr_fserv                                                           | t
+ user mapping               |            |                   | regress_addr_user on server integer                                  | t
+ foreign-data wrapper       |            | addr_fdw          | addr_fdw                                                             | t
+ access method              |            | btree             | btree                                                                | t
+ operator of access method  |            |                   | operator 1 (integer, integer) of pg_catalog.integer_ops USING btree  | t
+ function of access method  |            |                   | function 2 (integer, integer) of pg_catalog.integer_ops USING btree  | t
+ default value              |            |                   | for addr_nsp.gentable.b                                              | t
+ cast                       |            |                   | (bigint AS integer)                                                  | t
+ table constraint           | addr_nsp   |                   | a_chk on addr_nsp.gentable                                           | t
+ domain constraint          | addr_nsp   |                   | domconstr on addr_nsp.gendomain                                      | t
+ conversion                 | pg_catalog | koi8_r_to_mic     | pg_catalog.koi8_r_to_mic                                             | t
+ language                   |            | plpgsql           | plpgsql                                                              | t
+ schema                     |            | addr_nsp          | addr_nsp                                                             | t
+ operator class             | pg_catalog | int4_ops          | pg_catalog.int4_ops USING btree                                      | t
+ operator                   | pg_catalog |                   | pg_catalog.+(integer,integer)                                        | t
+ rule                       |            |                   | "_RETURN" on addr_nsp.genview                                        | t
+ trigger                    |            |                   | t on addr_nsp.gentable                                               | t
+ operator family            | pg_catalog | integer_ops       | pg_catalog.integer_ops USING btree                                   | t
+ policy                     |            |                   | genpol on addr_nsp.gentable                                          | t
+ statistics object          | addr_nsp   | gentable_stat     | addr_nsp.gentable_stat                                               | t
+ collation                  | pg_catalog | "default"         | pg_catalog."default"                                                 | t
+ transform                  |            |                   | for integer on language sql                                          | t
+ text search dictionary     | addr_nsp   | addr_ts_dict      | addr_nsp.addr_ts_dict                                                | t
+ text search parser         | addr_nsp   | addr_ts_prs       | addr_nsp.addr_ts_prs                                                 | t
+ text search configuration  | addr_nsp   | addr_ts_conf      | addr_nsp.addr_ts_conf                                                | t
+ text search template       | addr_nsp   | addr_ts_temp      | addr_nsp.addr_ts_temp                                                | t
+ subscription               |            | regress_addr_sub  | regress_addr_sub                                                     | t
+ publication                |            | addr_pub          | addr_pub                                                             | t
+ publication relation       |            |                   | addr_nsp.gentable in publication addr_pub                            | t
+ publication namespace      |            |                   | addr_nsp in publication addr_pub_schema                              | t
+ column master key          |            | addr_cmk          | addr_cmk                                                             | t
+ column encryption key      |            | addr_cek          | addr_cek                                                             | t
+ column encryption key data |            |                   | of addr_cek for addr_cmk                                             | t
+(53 rows)
 
 ---
 --- Cleanup resources
@@ -513,6 +548,8 @@ drop cascades to user mapping for regress_addr_user on server integer
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 DROP SCHEMA addr_nsp CASCADE;
 NOTICE:  drop cascades to 14 other objects
 DETAIL:  drop cascades to text search dictionary addr_ts_dict
@@ -548,6 +585,9 @@ WITH objects (classid, objid, objsubid) AS (VALUES
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
@@ -578,6 +618,7 @@ WITH objects (classid, objid, objsubid) AS (VALUES
     ('pg_event_trigger'::regclass, 0, 0), -- no event trigger
     ('pg_policy'::regclass, 0, 0), -- no policy
     ('pg_publication'::regclass, 0, 0), -- no publication
+    ('pg_publication_namespace'::regclass, 0, 0), -- no publication namespace
     ('pg_publication_rel'::regclass, 0, 0), -- no publication relation
     ('pg_subscription'::regclass, 0, 0), -- no subscription
     ('pg_transform'::regclass, 0, 0) -- no transformation
@@ -629,5 +670,9 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(subscription,,,)")|("(subscription,,)")|NULL
 ("(publication,,,)")|("(publication,,)")|NULL
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
+("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
+("(""column master key"",,,)")|("(""column master key"",,)")|NULL
+("(""column encryption key"",,,)")|("(""column encryption key"",,)")|NULL
+("(""column encryption key data"",,,)")|("(""column encryption key data"",,)")|NULL
 -- restore normal output mode
 \a\t
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be3e..2aa0e1632317 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 330eb0f7656b..646507f0290c 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 5815e17b39cc..4482a65d2459 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -162,26 +162,26 @@ PREPARE q7(unknown) AS
 -- DML statements
 PREPARE q8 AS
     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;
-SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements
+SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns, result_types FROM pg_prepared_statements
     ORDER BY name;
- name |                            statement                             |                  parameter_types                   |                                                       result_types                                                       
-------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
- q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
-      |         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} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |         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}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
- q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
- q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
- q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+ name |                            statement                             |                  parameter_types                   | parameter_orig_tables | parameter_orig_columns |                                                       result_types                                                       
+------+------------------------------------------------------------------+----------------------------------------------------+-----------------------+------------------------+--------------------------------------------------------------------------------------------------------------------------
+ q2   | PREPARE q2(text) AS                                             +| {text}                                             | {-}                   | {0}                    | {name,boolean,boolean}
+      |         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}            | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
+      |         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}                  | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
+      |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    |                       |                        | 
+      |         ORDER BY unique1;                                        |                                                    |                       |                        | 
+ q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {-,-}                 | {0,0}                  | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    |                       |                        | 
+ q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {-}                   | {0}                    | {text,path}
+      |     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;
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7f5700edc12..2ee5f7546d5b 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 9dd137415e86..7ebf6fdb25a1 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1423,11 +1423,13 @@ pg_prepared_statements| SELECT p.name,
     p.statement,
     p.prepare_time,
     p.parameter_types,
+    p.parameter_orig_tables,
+    p.parameter_orig_columns,
     p.result_types,
     p.from_sql,
     p.generic_plans,
     p.custom_plans
-   FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, result_types, from_sql, generic_plans, custom_plans);
+   FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, parameter_orig_tables, parameter_orig_columns, result_types, from_sql, generic_plans, custom_plans);
 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 d3ac08c9ee3e..357095e1b9b6 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -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,
@@ -171,10 +173,12 @@ WHERE t1.typinput = p1.oid AND t1.typtype in ('b', 'p') AND NOT
     (t1.typelem != 0 AND t1.typlen < 0) AND NOT
     (p1.prorettype = t1.oid AND NOT p1.proretset)
 ORDER BY 1;
- oid  |  typname  | oid | proname 
-------+-----------+-----+---------
- 1790 | refcursor |  46 | textin
-(1 row)
+ oid  |     typname      | oid  | proname 
+------+------------------+------+---------
+ 1790 | refcursor        |   46 | textin
+ 8243 | pg_encrypted_det | 1244 | byteain
+ 8244 | pg_encrypted_rnd | 1244 | byteain
+(3 rows)
 
 -- Varlena array types will point to array_in
 -- Exception as of 8.1: int2vector and oidvector have their own I/O routines
@@ -223,10 +227,12 @@ WHERE t1.typoutput = p1.oid AND t1.typtype in ('b', 'p') AND NOT
       (p1.oid = 'array_out'::regproc AND
        t1.typelem != 0 AND t1.typlen = -1)))
 ORDER BY 1;
- oid  |  typname  | oid | proname 
-------+-----------+-----+---------
- 1790 | refcursor |  47 | textout
-(1 row)
+ oid  |     typname      | oid | proname  
+------+------------------+-----+----------
+ 1790 | refcursor        |  47 | textout
+ 8243 | pg_encrypted_det |  31 | byteaout
+ 8244 | pg_encrypted_rnd |  31 | byteaout
+(3 rows)
 
 SELECT t1.oid, t1.typname, p1.oid, p1.proname
 FROM pg_type AS t1, pg_proc AS p1
@@ -287,10 +293,12 @@ WHERE t1.typreceive = p1.oid AND t1.typtype in ('b', 'p') AND NOT
     (t1.typelem != 0 AND t1.typlen < 0) AND NOT
     (p1.prorettype = t1.oid AND NOT p1.proretset)
 ORDER BY 1;
- oid  |  typname  | oid  | proname  
-------+-----------+------+----------
- 1790 | refcursor | 2414 | textrecv
-(1 row)
+ oid  |     typname      | oid  |  proname  
+------+------------------+------+-----------
+ 1790 | refcursor        | 2414 | textrecv
+ 8243 | pg_encrypted_det | 2412 | bytearecv
+ 8244 | pg_encrypted_rnd | 2412 | bytearecv
+(3 rows)
 
 -- Varlena array types will point to array_recv
 -- Exception as of 8.1: int2vector and oidvector have their own I/O routines
@@ -348,10 +356,12 @@ WHERE t1.typsend = p1.oid AND t1.typtype in ('b', 'p') AND NOT
       (p1.oid = 'array_send'::regproc AND
        t1.typelem != 0 AND t1.typlen = -1)))
 ORDER BY 1;
- oid  |  typname  | oid  | proname  
-------+-----------+------+----------
- 1790 | refcursor | 2415 | textsend
-(1 row)
+ oid  |     typname      | oid  |  proname  
+------+------------------+------+-----------
+ 1790 | refcursor        | 2415 | textsend
+ 8243 | pg_encrypted_det | 2413 | byteasend
+ 8244 | pg_encrypted_rnd | 2413 | byteasend
+(3 rows)
 
 SELECT t1.oid, t1.typname, p1.oid, p1.proname
 FROM pg_type AS t1, pg_proc AS p1
@@ -707,6 +717,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 9f644a0c1b2c..e68bccfdc879 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 a4b354c9e6c0..8ad1f458d503 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 000000000000..d056737ad514
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,124 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE ROLE regress_enc_user1;
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk2 WITH (
+    realm = 'test2'
+);
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'test2'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\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
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+
+\d tbl_447f
+\d+ tbl_447f
+
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+
+SET ROLE regress_enc_user1;
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET ROLE;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+SET ROLE regress_enc_user1;
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET ROLE;
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index e91072a75d1d..c9f5f0ced9f2 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -40,6 +40,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk WITH (realm = '');
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -98,7 +100,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 		('text search template'), ('text search configuration'),
 		('policy'), ('user mapping'), ('default acl'), ('transform'),
 		('operator of access method'), ('function of access method'),
-		('publication namespace'), ('publication relation')
+		('publication namespace'), ('publication relation'),
+		('column encryption key'), ('column encryption key data'), ('column master key')
 	LOOP
 		FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
 		LOOP
@@ -143,6 +146,10 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 SELECT pg_get_object_address('publication', '{one,two}', '{}');
 SELECT pg_get_object_address('subscription', '{one}', '{}');
 SELECT pg_get_object_address('subscription', '{one,two}', '{}');
+SELECT pg_get_object_address('column encryption key', '{one}', '{}');
+SELECT pg_get_object_address('column encryption key', '{one,two}', '{}');
+SELECT pg_get_object_address('column master key', '{one}', '{}');
+SELECT pg_get_object_address('column master key', '{one,two}', '{}');
 
 -- test successful cases
 WITH objects (type, name, args) AS (VALUES
@@ -166,6 +173,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 				('type', '{addr_nsp.genenum}', '{}'),
 				('cast', '{int8}', '{int4}'),
 				('collation', '{default}', '{}'),
+				('column encryption key', '{addr_cek}', '{}'),
+				('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+				('column master key', '{addr_cmk}', '{}'),
 				('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
 				('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
 				('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -219,6 +229,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 
 DROP SCHEMA addr_nsp CASCADE;
 
@@ -243,6 +255,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
@@ -273,6 +288,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('pg_event_trigger'::regclass, 0, 0), -- no event trigger
     ('pg_policy'::regclass, 0, 0), -- no policy
     ('pg_publication'::regclass, 0, 0), -- no publication
+    ('pg_publication_namespace'::regclass, 0, 0), -- no publication namespace
     ('pg_publication_rel'::regclass, 0, 0), -- no publication relation
     ('pg_subscription'::regclass, 0, 0), -- no subscription
     ('pg_transform'::regclass, 0, 0) -- no transformation
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95cee..7db788735c7f 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -75,7 +75,7 @@ CREATE TEMPORARY TABLE q5_prep_nodata AS EXECUTE q5(200, 'DTAAAA')
 PREPARE q8 AS
     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;
 
-SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements
+SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns, result_types 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 1149c6a839ef..e88c70d3024c 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 5edc1f1f6ed0..0ffe45fd3707 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -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,

base-commit: 4148c8b3daf95ca308f055e103f6ee82e25b8f99
-- 
2.37.3



Attachments:

  [text/plain] v9-0001-Transparent-column-encryption.patch (384.9K, ../[email protected]/2-v9-0001-Transparent-column-encryption.patch)
  download | inline diff:
From 6a055b8d12d26c3728f517432cee5424628f02da Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Tue, 27 Sep 2022 15:48:44 +0200
Subject: [PATCH v9] Transparent column encryption

This feature enables the automatic, transparent encryption and
decryption of particular columns in the client.  The data for those
columns then only ever appears in ciphertext on the server, so it is
protected from DBAs, sysadmins, cloud operators, etc. as well as
accidental leakage to server logs, file-system backups, etc.  The
canonical use case for this feature is storing credit card numbers
encrypted, in accordance with PCI DSS, as well as similar situations
involving social security numbers etc.  One can't do any computations
with encrypted values on the server, but for these use cases, that is
not necessary.  This feature does support deterministic encryption as
an alternative to the default randomized encryption, so in that mode
one can do equality lookups, at the cost of some security.

This functionality also exists in other database products, and the
overall concepts were mostly adopted from there.

(Note: This feature has nothing to do with any on-disk encryption
feature.  Both can exist independently.)

You declare a column as encrypted in a CREATE TABLE statement.  The
column value is encrypted by a symmetric key called the column
encryption key (CEK).  The CEK is a catalog object.  The CEK key
material is in turn encrypted by an assymmetric key called the column
master key (CMK).  The CMK is not stored in the database but somewhere
where the client can get to it, for example in a file or in a key
management system.  When a server sends rows containing encrypted
column values to the client, it first sends the required CMK and CEK
information (new protocol messages), which the client needs to record.
Then, the client can use this information to automatically decrypt the
incoming row data and forward it in plaintext to the application.

For the CMKs, libpq has a new connection parameter "cmklookup" that
specifies via a mini-language where to get the keys.  Right now, you
can use "file" to read it from a file, or "run" to run some program,
which could get it from a KMS.

The general idea would be for an application to have one CMK per area
of secret stuff, for example, for credit card data.  The CMK can be
rotated: each CEK can be represented multiple times in the database,
encrypted by a different CMK.  (The CEK can't be rotated easily, since
that would require reading out all the data from a table/column and
reencrypting it.  We could/should add some custom tooling for that,
but it wouldn't be a routine operation.)

Several encryption algorithms are provided.  The CMK process uses
PG_CMK_RSAES_OAEP_SHA_1 or _256.  The CEK process uses
AEAD_AES_*_CBC_HMAC_SHA_* with several strengths.

In the server, the encrypted datums are stored in types called
pg_encrypted_rnd and pg_encrypted_det (for randomized and
deterministic encryption).  These are essentially cousins of bytea.
For the rest of the database system below the protocol handling, there
is nothing special about those.  For example, pg_encrypted_rnd has no
operators at all, pg_encrypted_det has only an equality operator.
pg_attribute has a new column attrealtypid that stores the original
type of the data in the column.  This is only used for providing it to
clients, so that higher-level clients can convert the decrypted value
to their appropriate data types in their environments.

The protocol extensions are guarded by a new protocol extension option
"_pq_.column_encryption".  If this is not set, nothing changes, the
protocol stays the same, and no encryption or decryption happens.

The trickiest part of this whole thing appears to be how to get
transparently encrypted data into the database (as opposed to reading
it out).  It is required to use protocol-level prepared statements
(i.e., extended query) for this.  The client must first prepare a
statement, then describe the statement to get parameter metadata,
which indicates which parameters are to be encrypted and how.  So this
will require some care by applications that want to do this, but,
well, they probably should be careful anyway.  In libpq, the existing
APIs make this difficult, because there is no way to pass the result
of a describe-statement call back into
execute-statement-with-parameters.  I added new functions that do
this, so you then essentially do

    res0 = PQdescribePrepared(conn, "");
    res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, 0, res0);

(The name could obviously be improved.)  Other client APIs that have a
"statement handle" concept could do this more elegantly and probably
without any API changes.

Another challenge is that the parse analysis must check which
underlying column a parameter corresponds to.  This is similar to
resorigtbl and resorigcol in the opposite direction.  The current
implementation of this works for the test cases, but I know it has
some problems, so I'll continue working in this.  This functionality
is in principle available to all prepared-statement variants, not only
protocol-level.  So you can see in the tests that I expanded the
pg_prepared_statements view to show this information as well, which
also provides an easy way to test and debug this functionality
independent of column encryption.

psql doesn't use prepared statements, so writing into encrypted
columns wouldn't work at all via psql.  (Reading works no
problem.)  I added a new psql command \gencr that you can use like

INSERT INTO t1 VALUES ($1, $2) \gencr 'val1' 'val2'

TODO: This patch version has all the necessary pieces in place to make
this work, so you can have an idea how the overall system works.  It
contains some documentation and tests to help illustrate the
functionality.  Missing:

- psql \d support

Discussion: https://www.postgresql.org/message-id/flat/89157929-c2b6-817b-6025-8e4b2d89d88f%40enterprisedb.com
---
 doc/src/sgml/acronyms.sgml                    |  18 +
 doc/src/sgml/catalogs.sgml                    | 275 +++++++
 doc/src/sgml/datatype.sgml                    |  47 ++
 doc/src/sgml/ddl.sgml                         | 379 +++++++++
 doc/src/sgml/glossary.sgml                    |  23 +
 doc/src/sgml/libpq.sgml                       | 280 ++++++-
 doc/src/sgml/protocol.sgml                    | 392 +++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 186 +++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 117 +++
 .../ref/create_column_encryption_key.sgml     | 163 ++++
 .../sgml/ref/create_column_master_key.sgml    | 106 +++
 doc/src/sgml/ref/create_table.sgml            |  42 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/pg_dump.sgml                 |  31 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  33 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   7 +
 src/backend/access/common/printtup.c          | 191 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  64 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |   3 +
 src/backend/catalog/objectaddress.c           | 220 +++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  15 +
 src/backend/commands/colenccmds.c             | 354 ++++++++
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/prepare.c                |  74 +-
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              |  96 +++
 src/backend/commands/variable.c               |   7 +-
 src/backend/executor/spi.c                    |   4 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/analyze.c                  |   3 +-
 src/backend/parser/gram.y                     | 121 ++-
 src/backend/parser/parse_param.c              |  49 +-
 src/backend/parser/parse_target.c             |   6 +
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  57 +-
 src/backend/tcop/utility.c                    |  42 +-
 src/backend/utils/adt/arrayfuncs.c            |   1 +
 src/backend/utils/cache/lsyscache.c           | 111 +++
 src/backend/utils/cache/plancache.c           |  31 +-
 src/backend/utils/cache/syscache.c            |  58 ++
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |   6 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   4 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 350 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  31 +
 src/bin/pg_dump/pg_dump_sort.c                |  14 +
 src/bin/pg_dump/pg_dumpall.c                  |   5 +
 src/bin/pg_dump/t/002_pg_dump.pl              |  56 +-
 src/bin/psql/command.c                        |  31 +
 src/bin/psql/common.c                         |  39 +-
 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                   |  51 +-
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/meson.build               |   3 +
 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/commands/colenccmds.h             |  25 +
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  17 +
 src/include/parser/analyze.h                  |   4 +-
 src/include/parser/kwlist.h                   |   2 +
 src/include/parser/parse_node.h               |   2 +
 src/include/parser/parse_param.h              |   3 +-
 src/include/tcop/cmdtaglist.h                 |   6 +
 src/include/tcop/tcopprot.h                   |   2 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   6 +
 src/include/utils/plancache.h                 |   6 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  38 +-
 src/interfaces/libpq/fe-encrypt-openssl.c     | 766 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 593 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 180 +++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  22 +
 src/interfaces/libpq/libpq-int.h              |  37 +
 src/interfaces/libpq/meson.build              |   1 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |   2 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  29 +
 src/test/column_encryption/meson.build        |  20 +
 .../t/001_column_encryption.pl                | 183 +++++
 .../column_encryption/t/002_cmk_rotation.pl   | 108 +++
 src/test/column_encryption/test_client.c      | 210 +++++
 .../column_encryption/test_run_decrypt.pl     |  41 +
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 143 ++++
 src/test/regress/expected/object_address.out  | 153 ++--
 src/test/regress/expected/oidjoins.out        |   6 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/prepare.out         |  38 +-
 src/test/regress/expected/psql.out            |  25 +
 src/test/regress/expected/rules.out           |   4 +-
 src/test/regress/expected/type_sanity.out     |  46 +-
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 124 +++
 src/test/regress/sql/object_address.sql       |  18 +-
 src/test/regress/sql/prepare.sql              |   2 +-
 src/test/regress/sql/psql.sql                 |  17 +
 src/test/regress/sql/type_sanity.sql          |   2 +
 135 files changed, 7633 insertions(+), 208 deletions(-)
 create mode 100644 doc/src/sgml/ref/alter_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/alter_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/create_column_master_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_encryption_key.sgml
 create mode 100644 doc/src/sgml/ref/drop_column_master_key.sgml
 create mode 100644 src/backend/commands/colenccmds.c
 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/include/commands/colenccmds.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/meson.build
 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

diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 2df6559accce..bd1a0185ed7a 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -56,6 +56,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CEK</acronym></term>
+    <listitem>
+     <para>
+      Column Encryption Key
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CIDR</acronym></term>
     <listitem>
@@ -67,6 +76,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CMK</acronym></term>
+    <listitem>
+     <para>
+      Column Master Key
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CPAN</acronym></term>
     <listitem>
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 00f833d210e7..638be1c8507b 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -105,6 +105,21 @@ <title>System Catalogs</title>
       <entry>collations (locale information)</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link></entry>
+      <entry>column encryption keys</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link></entry>
+      <entry>column encryption key data</entry>
+     </row>
+
+     <row>
+      <entry><link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link></entry>
+      <entry>column master keys</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-constraint"><structname>pg_constraint</structname></link></entry>
       <entry>check constraints, unique constraints, primary key constraints, foreign key constraints</entry>
@@ -1360,6 +1375,40 @@ <title><structname>pg_attribute</structname> Columns</title>
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attcek</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       If the column is encrypted, a reference to the column encryption key, else 0.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attrealtypid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-type"><structname>pg_type</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       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 <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.  If the
+       column is not encrypted, then 0.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attencalg</structfield> <type>int16</type>
+      </para>
+      <para>
+       If the column is encrypted, the identifier of the encryption algorithm;
+       see <xref linkend="protocol-cek"/> for possible values.
+       </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>attinhcount</structfield> <type>int4</type>
@@ -2456,6 +2505,232 @@ <title><structname>pg_collation</structname> Columns</title>
   </para>
  </sect1>
 
+ <sect1 id="catalog-pg-colenckey">
+  <title><structname>pg_colenckey</structname></title>
+
+  <indexterm zone="catalog-pg-colenckey">
+   <primary>pg_colenckey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckey</structname> contains information
+   about the column encryption keys in the database.  The actual key material
+   of the column encryption keys is in the catalog <link
+   linkend="catalog-pg-colenckeydata"><structname>pg_colenckeydata</structname></link>.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column encryption key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column encryption key
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colenckeydata">
+  <title><structname>pg_colenckeydata</structname></title>
+
+  <indexterm zone="catalog-pg-colenckeydata">
+   <primary>pg_colenckeydata</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colenckeydata</structname> 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, <literal>(ckdcekid, ckdcmkid)</literal> is a
+   unique key of this table.
+  </para>
+
+  <para>
+   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.
+  </para>
+
+  <table>
+   <title><structname>pg_colenckeydata</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcekid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colenckey"><structname>pg_colenckey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column encryption key this entry belongs to
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkid</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-colmasterkey"><structname>pg_colmasterkey</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The column master key that the key material is encrypted with
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdcmkalg</structfield> <type>int16</type>
+      </para>
+      <para>
+       The encryption algorithm used for encrypting the key material; see
+       <xref linkend="protocol-cmk"/> for possible values.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>ckdencval</structfield> <type>bytea</type>
+      </para>
+      <para>
+       The key material of this column encryption key, encrypted using the
+       referenced column master key
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
+ <sect1 id="catalog-pg-colmasterkey">
+  <title><structname>pg_colmasterkey</structname></title>
+
+  <indexterm zone="catalog-pg-colmasterkey">
+   <primary>pg_colmasterkey</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_colmasterkey</structname> 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.
+  </para>
+
+  <table>
+   <title><structname>pg_colmasterkey</structname> Columns</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>oid</structfield> <type>oid</type>
+      </para>
+      <para>
+       Row identifier
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkname</structfield> <type>name</type>
+      </para>
+      <para>
+       Column master key name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkowner</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-authid"><structname>pg_authid</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       Owner of the column master key
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkrealm</structfield> <type>text</type>
+      </para>
+      <para>
+       A <quote>realm</quote> 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.
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 0258b192e0e4..05286dd3e530 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5346,4 +5346,51 @@ <title>Pseudo-Types</title>
 
   </sect1>
 
+  <sect1 id="datatype-encrypted">
+   <title>Types Related to Encryption</title>
+
+   <para>
+    An encrypted column value (see <xref linkend="ddl-column-encryption"/>) is
+    internally stored using the types
+    <type>pg_encrypted_rnd</type> (for randomized encryption) or
+    <type>pg_encrypted_det</type> (for deterministic encryption); see <xref
+    linkend="datatype-encrypted-table"/>.  Most of the database system treats
+    this as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  The external
+    representation of these types is the same as the <type>bytea</type> 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.
+   </para>
+
+    <table id="datatype-encrypted-table">
+     <title>Types Related to Encryption</title>
+     <tgroup cols="3">
+      <colspec colname="col1" colwidth="1*"/>
+      <colspec colname="col2" colwidth="3*"/>
+      <colspec colname="col3" colwidth="2*"/>
+      <thead>
+       <row>
+        <entry>Name</entry>
+        <entry>Storage Size</entry>
+        <entry>Description</entry>
+       </row>
+      </thead>
+      <tbody>
+       <row>
+        <entry><type>pg_encrypted_det</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, deterministic encryption</entry>
+       </row>
+       <row>
+        <entry><type>pg_encrypted_rnd</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, randomized encryption</entry>
+       </row>
+      </tbody>
+     </tgroup>
+    </table>
+  </sect1>
  </chapter>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 03c01937094b..8cf69fe10fef 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1211,6 +1211,385 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Transparent Column Encryption</title>
+
+  <para>
+   With <firstterm>transparent column encryption</firstterm>, 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.
+  </para>
+
+  <sect2>
+   <title>Using Transparent Column Encryption</title>
+
+  <para>
+   Tranparent column encryption uses two levels of cryptographic keys.  The
+   actual column value is encrypted using a symmetric algorithm, such as AES,
+   using a <firstterm>column encryption key</firstterm>
+   (<acronym>CEK</acronym>).  The column encryption key is in turn encrypted
+   using an assymmetric algorithm, such as RSA, using a <firstterm>column
+   master key</firstterm> (<acronym>CMK</acronym>).  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.
+  </para>
+
+  <para>
+   Here is an example declaring a column as encrypted:
+<programlisting>
+CREATE TABLE customers (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    creditcard_num text <emphasis>ENCRYPTED WITH (column_encryption_key = cek1)</emphasis>
+);
+</programlisting>
+  </para>
+
+  <para>
+   Column encryption supports <firstterm>randomized</firstterm>
+   (also known as <firstterm>probabilistic</firstterm>) and
+   <firstterm>deterministic</firstterm> 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:
+<programlisting>
+CREATE TABLE employees (
+    id int PRIMARY KEY,
+    name text NOT NULL,
+    ...
+    ssn text ENCRYPTED WITH (
+        column_encryption_key = cek1, <emphasis>encryption_type = deterministic</emphasis>)
+);
+</programlisting>
+  </para>
+
+  <para>
+   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.
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Reading and Writing Encrypted Columns</title>
+
+   <para>
+    Reading and writing encrypted columns is meant to be handled automatically
+    by the client library/driver and should be mostly transparent to the
+    application code, if certain prerequisites are fulfilled:
+
+    <itemizedlist>
+     <listitem>
+      <para>
+       The client library needs to support transparent column encryption.  Not
+       all client libraries do.  Furthermore, the client library might require
+       that transparent column encryption is explicitly enabled at connection
+       time.  See the documentation of the client library for details.
+      </para>
+     </listitem>
+
+     <listitem>
+      <para>
+       Column master keys and column encryption keys have been set up, and the
+       client library has been configured to be able to look up column master
+       keys from the key store or key management system.
+      </para>
+     </listitem>
+    </itemizedlist>
+   </para>
+
+   <para>
+    Reading from encrypted columns will then work automatically.  For example,
+    using the above example,
+<programlisting>
+SELECT ssn FROM employees WHERE id = 5;
+</programlisting>
+    will return the unencrypted value for the <literal>ssn</literal> column in
+    any rows found.
+   </para>
+
+   <para>
+    Writing to encrypted columns requires that the extended query protocol
+    (protocol-level prepared statements) be used, so that the values to be
+    encrypted are supplied separately from the SQL command.  For example,
+    using, say, psql or libpq, the following would not work:
+<programlisting>
+-- WRONG!
+INSERT INTO ssn (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This will leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of column encryption.
+    Note that using server-side prepared statements using the SQL commands
+    <command>PREPARE</command> and <command>EXECUTE</command> is equally
+    incorrect, since that would also leak the parameters provided to
+    <command>EXECUTE</command> to the server.
+   </para>
+
+   <para>
+    The correct notional order of operations is illustrated here using
+    libpq-like code (without error checking):
+<programlisting>
+PGresult   *tmpres,
+           *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+PQprepare(conn, "", "INSERT INTO ssn (id, name, ssn) VALUES ($1, $2, $3)", 3, NULL);
+
+/* This fetches information about which parameters need to be encrypted. */
+tmpres = PQdescribePrepared(conn, "");
+
+res = PQexecPrepared2(conn, "", 3, values, NULL, NULL, NULL, 0, tmpres);
+
+/* print result in res */
+</programlisting>
+    Higher-level client libraries might use the protocol-level prepared
+    statements automatically and thus won't require any code changes.
+   </para>
+
+   <para>
+    <application>psql</application> provides the command
+    <literal>\gencr</literal> to run prepared statements like this:
+<programlisting>
+INSERT INTO ssn (id, name, ssn) VALUES ($1, $2, $3) \gencr '1' 'Someone', '12345'
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Transparent Column Encryption</title>
+
+  <para>
+   The steps to set up transparent column encryption for a database are:
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK, for example, using a cryptographic
+      library or toolkit, or a key management system.  Secure access to the
+      key as appropriate, using access control, passwords, etc.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database using the SQL command <xref linkend="sql-create-column-master-key"/>.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the (unencrypted) key material for the CEK in a temporary
+      location.  (It will be encrypted in the next step.  Depending on the
+      available tools, it might be possible and sensible to combine these two
+      steps.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material using the CMK (created earlier).
+      (The unencrypted version of the CEK key material can now be disposed
+      of.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database using the SQL command <xref
+      linkend="sql-create-column-encryption-key"/>.  This command
+      <quote>uploads</quote> the encrypted CEK key material created in the
+      previous step to the database server.  The local copy of the CEK key
+      material can then be removed.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns using the created CEK.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the client library/driver to be able to look up the CMK
+      created earlier.
+     </para>
+    </listitem>
+   </orderedlist>
+
+   Once this is done, values can be written to and read from the encrypted
+   columns in a transparent way.
+  </para>
+
+  <para>
+   Note that these steps should not be run on the database server, but on some
+   client machine.  Neither the CMK nor the unencrypted CEK should ever appear
+   on the database server host.
+  </para>
+
+  <para>
+   The specific details of this setup depend on the desired CMK storage
+   mechanism/key management system as well as the client libraries to be used.
+   The following example uses the <command>openssl</command> command-line tool
+   to set up the keys.
+
+   <orderedlist>
+    <listitem>
+     <para>
+      Create the key material for the CMK and write it to a file:
+<programlisting>
+openssl genpkey -algorithm rsa -out cmk1.pem
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CMK in the database:
+<programlisting>
+psql ... -c "CREATE COLUMN MASTER KEY cmk1 WITH (realm = '')"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 32
+</programlisting>
+      (See <xref linkend="protocol-cek-table"/> for required key lengths.)
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Encrypt the created CEK key material:
+<programlisting>
+openssl pkeyutl -encrypt -inkey cmk1.pem -pkeyopt rsa_padding_mode:oaep -in cek1.bin -out cek1.bin.enc
+rm cek1.bin
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Register the CEK in the database:
+<programlisting>
+# convert file contents to hex encoding; this is just one possible way
+cekenchex=$(perl -0ne 'print unpack "H*"' cek1.bin.enc)
+psql ... -c "CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, encrypted_value = '\\x${cekenchex}')"
+rm cek1.bin.enc
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create encrypted columns as shown in the examples above.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Configure the libpq for CMK lookup (see also <xref linkend="libpq-connect-cmklookup"/>):
+<programlisting>
+PGCMKLOOKUP="*=file:$PWD/%k.pem"
+export PGCMKLOOKUP
+</programlisting>
+     </para>
+
+     <para>
+      Additionally, libpq requires that the connection parameter <xref
+      linkend="libpq-connect-column-encryption"/> be set in order to activate
+      the transparent column encryption functionality.  This should be done in
+      the connection parameters of the application, but an environment
+      variable (<envar>PGCOLUMNENCRYPTION</envar>) is also available.
+     </para>
+    </listitem>
+   </orderedlist>
+  </para>
+  </sect2>
+
+  <sect2>
+   <title>Guidance on Using Transparent Column Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use transparent column encryption, and what precautions need to be
+    taken to maintain its security.
+   </para>
+
+   <para>
+    In general, column encryption is never a replacement for additional
+    security and encryption techniques such as transmission encryption
+    (SSL/TLS), storage encryption, strong access control, and password
+    security.  Column encryption only targets specific use cases and should be
+    used in conjunction with additional security measures.
+   </para>
+
+   <para>
+    A typical use case for column encryption is to encrypt specific values
+    with additional security requirements, for example credit card numbers.
+    This would allow you to store that security-sensitive data together with
+    the rest of your data (thus getting various benefits, such as referential
+    integrity, consistent backups), while giving access to that data only to
+    specific clients and preventing accidental leakage on the server side
+    (server logs, file system backups, etc.).
+   </para>
+
+   <para>
+    Column encryption cannot hide the existence or absence of data, it can
+    only disguise the particular data that is known to exist.  For example,
+    storing a cleartext person name and an encrypted credit card number
+    indicates that the person has a credit card.  That might not reveal too
+    much if the database is for an online store and there is other data nearby
+    that shows that the person has recently made purchases.  But in another
+    example, storing a cleartext person name and an encrypted diagnosis in a
+    medical database probably indicates that the person has a medical issue.
+    Depending on the circumstances, that might not by itself be sufficient
+    security.
+   </para>
+
+   <para>
+    Encryption cannot completely hide the length of values.  The encryption
+    methods will pad values to multiples of the underlying cipher's block size
+    (usually 16 bytes), so some length differences will be unified this way.
+    There is no concern if all values are of the same length (e.g., credit
+    card numbers).  But if there are signficant length differences between
+    valid values and that length information is security-sensitive, then
+    application-specific workarounds such as padding would need to be applied.
+    How to do that securely is beyond the scope of this manual.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so is usually
+     subject to government or industry regulations.  This section is not meant
+     to provide complete instructions on how to do this correctly.  Please
+     seek additional advice when engaging in such projects.
+    </para>
+   </note>
+  </sect2>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index d6d0a3a8140c..10507e43911e 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -353,6 +353,29 @@ <title>Glossary</title>
    </glossdef>
   </glossentry>
 
+  <glossentry id="glossary-column-encryption-key">
+   <glossterm>Column encryption key</glossterm>
+   <glossdef>
+    <para>
+     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.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt column encryption keys.  (So the
+     column master key is a <firstterm>key encryption key</firstterm>.)
+     Column master keys are stored outside the database system, for example in
+     a key management system.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-commit">
    <glossterm>Commit</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 57bfc8fc714d..1076d4664079 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,123 @@ <title>Parameter Key Words</title>
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-connect-column-encryption" xreflabel="column_encryption">
+      <term><literal>column_encryption</literal></term>
+      <listitem>
+       <para>
+        If set to 1, this enables transparent column encryption for the
+        connection.  If encrypted columns are queried and this is not enabled,
+        the encrypted value is returned.  See <xref
+        linkend="ddl-column-encryption"/> for more information about this
+        feature.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-cmklookup" xreflabel="cmklookup">
+      <term><literal>cmklookup</literal></term>
+      <listitem>
+       <para>
+        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 <literal>key=value</literal> entries separated
+        by semicolons.  Each key is the name of a key realm, or
+        <literal>*</literal> to match all realms.  The value is a
+        <literal>scheme:data</literal> 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:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>%a</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name (see <xref linkend="protocol-cmk-table"/>)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%b</literal></term>
+          <listitem>
+           <para>
+            The encrypted CEK data in Base64 encoding (only for the <literal>run</literal> scheme)
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%r</literal></term>
+          <listitem>
+           <para>
+            The realm name
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        Available schemes are:
+        <variablelist>
+         <varlistentry>
+          <term><literal>file</literal></term>
+          <listitem>
+           <para>
+            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.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>run</literal></term>
+          <listitem>
+           <para>
+            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.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <para>
+        The default value is empty.
+       </para>
+
+       <para>
+        Example:
+<programlisting>
+cmklookup="r1=file:/some/where/secrets/%k.pem;*=file:/else/where/%r/%k.pem"
+</programlisting>
+        This specification says, for keys in realm <quote>r1</quote>, load
+        them from the specified file, replacing <literal>%k</literal> by the
+        key name.  For keys in other realms, load them from the file,
+        replacing realm and key names as specified.
+       </para>
+
+       <para>
+        An example for interacting with a (hypothetical) key management
+        system:
+<programlisting>
+cmklookup="*=run:acmekms decrypt --key %k --alg %a --blob '%b'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -3012,6 +3129,57 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPrepared2">
+      <term><function>PQexecPrepared2</function><indexterm><primary>PQexecPrepared2</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Sends a request to execute a prepared statement with given
+        parameters, and waits for the result, with support for encrypted columns.
+<synopsis>
+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);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPrepared2"/> is like <xref
+        linkend="libpq-PQexecPrepared"/> with additional support for encrypted
+        columns.  The parameter <parameter>paramDesc</parameter> must be a
+        result set obtained from <xref linkend="libpq-PQdescribePrepared"/> on
+        the same prepared statement.
+       </para>
+
+       <para>
+        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.
+       </para>
+
+       <para>
+        The parameter <parameter>paramForceColumnEncryptions</parameter>
+        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.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3862,6 +4030,28 @@ <title>Retrieving Query Result Information</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQfisencrypted">
+     <term><function>PQfisencrypted</function><indexterm><primary>PQfisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given column came from an encrypted
+       column.  Column numbers start at 0.
+<synopsis>
+int PQfisencrypted(const PGresult *res,
+                   int column_number);
+</synopsis>
+      </para>
+
+      <para>
+       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.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQfsize">
      <term><function>PQfsize</function><indexterm><primary>PQfsize</primary></indexterm></term>
 
@@ -4037,12 +4227,37 @@ <title>Retrieving Query Result Information</title>
 
       <para>
        This function is only useful when inspecting the result of
-       <xref linkend="libpq-PQdescribePrepared"/>.  For other types of queries it
+       <xref linkend="libpq-PQdescribePrepared"/>.  For other types of results it
        will return zero.
       </para>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQparamisencrypted">
+     <term><function>PQparamisencrypted</function><indexterm><primary>PQparamisencrypted</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Returns whether the value for the given parameter is destined for an
+       encrypted column.  Parameter numbers start at 0.
+<synopsis>
+int PQparamisencrypted(const PGresult *res, int param_number);
+</synopsis>
+      </para>
+
+      <para>
+       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 <xref linkend="libpq-PQexecPrepared2"/> for another way to do that.)
+       This function is only useful when inspecting the result of <xref
+       linkend="libpq-PQdescribePrepared"/>.  For other types of results it
+       will return false.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQprint">
      <term><function>PQprint</function><indexterm><primary>PQprint</primary></indexterm></term>
 
@@ -4568,6 +4783,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPrepared2"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4575,6 +4791,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPrepared2"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4685,6 +4902,46 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPrepared2">
+     <term><function>PQsendQueryPrepared2</function><indexterm><primary>PQsendQueryPrepared2</primary></indexterm></term>
+
+     <listitem>
+      <para>
+       Sends a request to execute a prepared statement with given
+       parameters, without waiting for the result(s), with support for encrypted columns.
+<synopsis>
+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);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPrepared2"/> is like <xref
+       linkend="libpq-PQsendQueryPrepared"/> with additional support for encrypted
+       columns.  The parameter <parameter>paramDesc</parameter> must be a
+       result set obtained from <xref linkend="libpq-PQsendDescribePrepared"/> on
+       the same prepared statement.
+      </para>
+
+      <para>
+       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 <xref linkend="libpq-PQexecPrepared2"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4735,6 +4992,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPrepared2"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7767,6 +8025,26 @@ <title>Environment Variables</title>
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCMKLOOKUP</envar></primary>
+      </indexterm>
+      <envar>PGCMKLOOKUP</envar> behaves the same as the <xref
+      linkend="libpq-connect-cmklookup"/> connection parameter.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGCOLUMNENCRYPTION</envar></primary>
+      </indexterm>
+      <envar>PGCOLUMNENCRYPTION</envar> behaves the same as the <xref
+      linkend="libpq-connect-column-encryption"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 75caa7fdb679..6aa66058d300 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1108,6 +1108,68 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-transparent-column-encryption-flow">
+   <title>Transparent Column Encryption</title>
+
+   <para>
+    Transparent column encryption is enabled by sending the parameter
+    <literal>_pq_.column_encryption</literal> with a value of
+    <literal>1</literal> in the StartupMessage.  This is a protocol extension
+    that enables a few additional protocol messages and adds additional fields
+    to existing protocol messages.  Client drivers should only activate this
+    protocol extension when requested by the user, for example through a
+    connection parameter.
+   </para>
+
+   <para>
+    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.)
+   </para>
+
+   <para>
+    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.
+   </para>
+
+   <para>
+    When column encryption is used, the plaintext is always in text format
+    (not binary format).
+   </para>
+
+   <para>
+    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 <literal>100</literal> and
+    an integer represented by the string <literal>+100</literal> would result
+    in two different ciphertexts, thus defeating the main point of
+    deterministic encryption.  This protocol specification requires the
+    plaintext to be in <quote>canonical</quote> form, which is the form that
+    is produced by the server when it outputs a particular value in text
+    format.
+   </para>
+
+   <para>
+    When transparent column encryption is enabled, the client encoding must
+    match the server encoding.  This ensures that all values encrypted or
+    decrypted by the client match the server encoding.
+   </para>
+
+   <para>
+    The cryptographic operations used for transparent column encryption are
+    described in <xref linkend="protocol-transparent-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2>
    <title>Function Call</title>
 
@@ -4055,6 +4117,140 @@ <title>Message Formats</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ColumnEncryptionKey">
+    <term>ColumnEncryptionKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-transparent-column-encryption-flow"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('Y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column encryption key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The identifier of the master key used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         The identifier of the algorithm used to encrypt this key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The length of the following key material.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Byte<replaceable>n</replaceable></term>
+       <listitem>
+        <para>
+         The key material, encrypted with the master key referenced above.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry id="protocol-message-formats-ColumnMasterKey">
+    <term>ColumnMasterKey (B)</term>
+    <listitem>
+     <para>
+      This message can only appear if the protocol extension
+      <literal>_pq_.column_encryption</literal> is enabled.  (See <xref
+      linkend="protocol-transparent-column-encryption-flow"/>.)
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Byte1('y')</term>
+       <listitem>
+        <para>
+         Identifies the message as a column master key message.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         Length of message contents in bytes, including self.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         The session-specific identifier of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The name of the key.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>String</term>
+       <listitem>
+        <para>
+         The key's realm.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="protocol-message-formats-CommandComplete">
     <term>CommandComplete (B)</term>
     <listitem>
@@ -5151,6 +5347,46 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref
+      linkend="protocol-transparent-column-encryption-flow"/>), then there is
+      also the following for each parameter:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         If this parameter is to be encrypted, this specifies the
+         identifier of the encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         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.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5539,6 +5775,35 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref
+      linkend="protocol-transparent-column-encryption-flow"/>), then there is
+      also the following for each field:
+     </para>
+
+     <variablelist>
+      <varlistentry>
+       <term>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         column encryption key to use, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term>Int16</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encrypt algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7342,6 +7607,133 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-transparent-column-encryption-crypto">
+  <title>Transparent Column Encryption Cryptography</title>
+
+  <para>
+   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.
+  </para>
+
+  <para>
+   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.
+  </para>
+
+  <sect2 id="protocol-cmk">
+   <title>Column Master Keys</title>
+
+   <para>
+    The currently defined algorithms for column master keys are listed in
+    <xref linkend="protocol-cmk-table"/>.
+   </para>
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="3">
+     <thead>
+      <row>
+       <entry>ID</entry>
+       <entry>Name</entry>
+       <entry>Reference</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/rfc8017">RFC 8017</ulink> (PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/rfc8017">RFC 8017</ulink> (PKCS #1)</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+  </sect2>
+
+  <sect2 id="protocol-cek">
+   <title>Column Encryption Keys</title>
+
+   <para>
+    The currently defined algorithms for column encryption keys are listed in
+    <xref linkend="protocol-cek-table"/>.
+   </para>
+
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="3">
+     <thead>
+      <row>
+       <entry>ID</entry>
+       <entry>Name</entry>
+       <entry>Reference</entry>
+       <entry>Key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>130</entry>
+       <entry><literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>32</entry>
+      </row>
+      <row>
+       <entry>131</entry>
+       <entry><literal>AEAD_AES_192_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>132</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_384</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>56</entry>
+      </row>
+      <row>
+       <entry>133</entry>
+       <entry><literal>AEAD_AES_256_CBC_HMAC_SHA_512</literal></entry>
+       <entry><ulink url="https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05"/></entry>
+       <entry>64</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <para>
+    The <quote>associated data</quote> in these algorithms consists of 4
+    bytes: The ASCII letters <literal>P</literal> and <literal>G</literal>
+    (byte values 80 and 71), followed by the algorithm ID as a 16-bit unsigned
+    integer in network byte order.
+   </para>
+
+   <para>
+    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
+<programlisting>
+SUBSTRING(<replaceable>HMAC</replaceable>(<replaceable>K</replaceable>, <replaceable>P</replaceable>) FOR <replaceable>IVLEN</replaceable>)
+</programlisting>
+    where <replaceable>HMAC</replaceable> is the HMAC function associated with
+    the algorithm, <replaceable>K</replaceable> is the prefix of the CEK of
+    the length required by the HMAC function, and <replaceable>P</replaceable>
+    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.)
+   </para>
+  </sect2>
+ </sect1>
+
  <sect1 id="protocol-changes">
   <title>Summary of Changes since Protocol 2.0</title>
 
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index e90a0e1f8372..331b1f010b5c 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -8,6 +8,8 @@
 <!ENTITY abort              SYSTEM "abort.sgml">
 <!ENTITY alterAggregate     SYSTEM "alter_aggregate.sgml">
 <!ENTITY alterCollation     SYSTEM "alter_collation.sgml">
+<!ENTITY alterColumnEncryptionKey SYSTEM "alter_column_encryption_key.sgml">
+<!ENTITY alterColumnMasterKey SYSTEM "alter_column_master_key.sgml">
 <!ENTITY alterConversion    SYSTEM "alter_conversion.sgml">
 <!ENTITY alterDatabase      SYSTEM "alter_database.sgml">
 <!ENTITY alterDefaultPrivileges SYSTEM "alter_default_privileges.sgml">
@@ -62,6 +64,8 @@
 <!ENTITY createAggregate    SYSTEM "create_aggregate.sgml">
 <!ENTITY createCast         SYSTEM "create_cast.sgml">
 <!ENTITY createCollation    SYSTEM "create_collation.sgml">
+<!ENTITY createColumnEncryptionKey SYSTEM "create_column_encryption_key.sgml">
+<!ENTITY createColumnMasterKey SYSTEM "create_column_master_key.sgml">
 <!ENTITY createConversion   SYSTEM "create_conversion.sgml">
 <!ENTITY createDatabase     SYSTEM "create_database.sgml">
 <!ENTITY createDomain       SYSTEM "create_domain.sgml">
@@ -109,6 +113,8 @@
 <!ENTITY dropAggregate      SYSTEM "drop_aggregate.sgml">
 <!ENTITY dropCast           SYSTEM "drop_cast.sgml">
 <!ENTITY dropCollation      SYSTEM "drop_collation.sgml">
+<!ENTITY dropColumnEncryptionKey SYSTEM "drop_column_encryption_key.sgml">
+<!ENTITY dropColumnMasterKey SYSTEM "drop_column_master_key.sgml">
 <!ENTITY dropConversion     SYSTEM "drop_conversion.sgml">
 <!ENTITY dropDatabase       SYSTEM "drop_database.sgml">
 <!ENTITY dropDomain         SYSTEM "drop_domain.sgml">
diff --git a/doc/src/sgml/ref/alter_column_encryption_key.sgml b/doc/src/sgml/ref/alter_column_encryption_key.sgml
new file mode 100644
index 000000000000..7597cd80ca6a
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,186 @@
+<!--
+doc/src/sgml/ref/alter_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-encryption-key">
+ <indexterm zone="sql-alter-column-encryption-key">
+  <primary>ALTER COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>change the definition of a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> ADD VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> DROP VALUE (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>
+)
+
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN ENCRYPTION KEY</command> changes the definition of a
+   column encryption key.
+  </para>
+
+  <para>
+   The first form adds new encrypted key data to a column encryption key,
+   which must be encrypted with a different column master key than the
+   existing key data.  The second form removes a key data entry for a given
+   column master key.  Together, these forms can be used for column master key
+   rotation.
+  </para>
+
+  <para>
+   You must own the column encryption key to use <command>ALTER COLUMN
+   ENCRYPTION KEY</command>.  To alter the owner, you must also be a direct or
+   indirect member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column encryption key's
+   database.  (These restrictions enforce that altering the owner doesn't do
+   anything you couldn't do by dropping and recreating the column encryption
+   key.  However, a superuser can alter ownership of any column encryption key
+   anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name of an existing column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  See <xref
+      linkend="sql-create-column-encryption-key"/> for details
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rotate the master keys used to encrypt a given column encryption key,
+   use a command sequence like this:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (
+    COLUMN_MASTER_KEY = cmk1
+);
+</programlisting>
+  </para>
+
+  <para>
+   To rename the column encryption key <literal>cek1</literal> to
+   <literal>cek2</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column encryption key <literal>cek1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN ENCRYPTION KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/alter_column_master_key.sgml b/doc/src/sgml/ref/alter_column_master_key.sgml
new file mode 100644
index 000000000000..13e310b22748
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,117 @@
+<!--
+doc/src/sgml/ref/alter_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-alter-column-master-key">
+ <indexterm zone="sql-alter-column-master-key">
+  <primary>ALTER COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>ALTER COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>ALTER COLUMN MASTER KEY</refname>
+  <refpurpose>change the definition of a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> RENAME TO <replaceable>new_name</replaceable>
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER }
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   You must own the column master key to use <command>ALTER COLUMN MASTER
+   KEY</command>.  To alter the owner, you must also be a direct or indirect
+   member of the new owning role, and that role must have
+   <literal>CREATE</literal> privilege on the column master key's database.
+   (These restrictions enforce that altering the owner doesn't do anything you
+   couldn't do by dropping and recreating the column master key.  However, a
+   superuser can alter ownership of any column master key anyway.)
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable class="parameter">name</replaceable></term>
+    <listitem>
+     <para>
+      The name (optionally schema-qualified) of an existing column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_name</replaceable></term>
+    <listitem>
+     <para>
+      The new name of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_owner</replaceable></term>
+    <listitem>
+     <para>
+      The new owner of the column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+   To rename the column master key <literal>cmk1</literal> to
+   <literal>cmk2</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk2;
+</programlisting>
+  </para>
+
+  <para>
+   To change the owner of the column master key <literal>cmk1</literal> to
+   <literal>joe</literal>:
+<programlisting>
+ALTER COLUMN MASTER KEY cmk1 OWNER TO joe;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>ALTER COLUMN MASTER KEY</command> statement in the
+   SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-create-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/ref/create_column_encryption_key.sgml b/doc/src/sgml/ref/create_column_encryption_key.sgml
new file mode 100644
index 000000000000..a94b0924b153
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,163 @@
+<!--
+doc/src/sgml/ref/create_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-encryption-key">
+ <indexterm zone="sql-create-column-encryption-key">
+  <primary>CREATE COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>define a new column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN ENCRYPTION KEY <replaceable>name</replaceable> WITH VALUES (
+    COLUMN_MASTER_KEY = <replaceable>cmk</replaceable>,
+    [ ALGORITHM = <replaceable>algorithm</replaceable>, ]
+    ENCRYPTED_VALUE = <replaceable>encval</replaceable>
+)
+[ , ... ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN ENCRYPTION KEY</command> defines a new column
+   encryption key.  A column encryption key is used for client-side encryption
+   of table columns that have been defined as encrypted.  The key material of
+   a column encryption key is stored in the database's system catalogs,
+   encrypted (wrapped) by a column master key (which in turn is only
+   accessible to the client, not the database server).
+  </para>
+
+  <para>
+   A column encryption key can be associated with more than one column master
+   key.  To specify that, specify more than one parenthesized definition (see
+   also the examples).
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>cmk</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the column master key that was used to encrypt this column
+      encryption key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>algorithm</replaceable></term>
+
+    <listitem>
+     <para>
+      The encryption algorithm that was used to encrypt the key material of
+      this column encryption key.  Supported algorithms are:
+      <itemizedlist>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal> (default)</para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal> (default)</para>
+       </listitem>
+      </itemizedlist>
+     </para>
+
+     <para>
+      This is informational only.  The specified value is provided to the
+      client, which may use it for decrypting the column encryption key on the
+      client side.  But a client is also free to ignore this information and
+      figure out how to arrange the decryption in some other way.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>encval</replaceable></term>
+
+    <listitem>
+     <para>
+      The key material of this column encryption key, encrypted with the
+      specified column master key using the specified algorithm.  The value
+      must be a <type>bytea</type>-compatible literal.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ENCRYPTED_VALUE = '\x01020204...'
+);
+</programlisting>
+  </para>
+
+  <para>
+   To specify more than one associated column master key:
+<programlisting>
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    COLUMN_MASTER_KEY = cmk1,
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ENCRYPTED_VALUE = '\xF1F2F2F4...'
+);
+</programlisting>
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-drop-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_column_master_key.sgml b/doc/src/sgml/ref/create_column_master_key.sgml
new file mode 100644
index 000000000000..ec5fa4cde571
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,106 @@
+<!--
+doc/src/sgml/ref/create_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-create-column-master-key">
+ <indexterm zone="sql-create-column-master-key">
+  <primary>CREATE COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>CREATE COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>CREATE COLUMN MASTER KEY</refname>
+  <refpurpose>define a new column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+CREATE COLUMN MASTER KEY <replaceable>name</replaceable> WITH (
+    [ REALM = <replaceable>realm</replaceable> ]
+)
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>CREATE COLUMN MASTER KEY</command> defines a new column master
+   key.  A column master key is used to encrypt column encryption keys, which
+   are the keys that actually encrypt the column data.  The key material of
+   the column master key is not stored in the database.  The definition of a
+   column master key records information that will allow a client to locate
+   the key material, for example in a file or in a key management system.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+  <variablelist>
+   <varlistentry>
+    <term><replaceable>name</replaceable></term>
+
+    <listitem>
+     <para>
+      The name of the new column master key.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable>realm</replaceable></term>
+
+    <listitem>
+     <para>
+      This is an optional string that can be used to organize column master
+      keys into groups for lookup by clients.  The intent is that all column
+      master keys that are stored in the same system (file system location,
+      key management system, etc.) should be in the same realm.  A client
+      would then be configured to look up all keys in a given realm in a
+      certain way.  See the documentation of the respective client library for
+      further usage instructions.
+     </para>
+
+     <para>
+      The default is the empty string.
+     </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+<programlisting>
+CREATE COLUMN MASTER KEY cmk1 (realm = 'myrealm');
+</programlisting>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>CREATE COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+  <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-drop-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index c14b2010d81d..feebf51fd9a5 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -22,7 +22,7 @@
  <refsynopsisdiv>
 <synopsis>
 CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] <replaceable class="parameter">table_name</replaceable> ( [
-  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
+  { <replaceable class="parameter">column_name</replaceable> <replaceable class="parameter">data_type</replaceable> [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ STORAGE { PLAIN | EXTERNAL | EXTENDED | MAIN } ] [ COMPRESSION <replaceable>compression_method</replaceable> ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -349,6 +349,46 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables transparent column encryption for the column.
+      <replaceable>encryption_options</replaceable> are comma-separated
+      <literal>key=value</literal> specifications.  The following options are
+      available:
+      <variablelist>
+       <varlistentry>
+        <term><literal>column_encryption_key</literal></term>
+        <listitem>
+         <para>
+          Specifies the name of the column encryption key to use.  Specifying
+          this is mandatory.
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>encryption_type</literal></term>
+        <listitem>
+         <para>
+          <literal>randomized</literal> (the default) or <literal>deterministic</literal>
+         </para>
+        </listitem>
+       </varlistentry>
+       <varlistentry>
+        <term><literal>algorithm</literal></term>
+        <listitem>
+         <para>
+          The encryption algorithm to use.  The default is
+          <literal>AEAD_AES_128_CBC_HMAC_SHA_256</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
diff --git a/doc/src/sgml/ref/drop_column_encryption_key.sgml b/doc/src/sgml/ref/drop_column_encryption_key.sgml
new file mode 100644
index 000000000000..9d157de9c5f8
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_encryption_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_encryption_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-encryption-key">
+ <indexterm zone="sql-drop-column-encryption-key">
+  <primary>DROP COLUMN ENCRYPTION KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN ENCRYPTION KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN ENCRYPTION KEY</refname>
+  <refpurpose>remove a column encryption key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN ENCRYPTION KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN ENCRYPTION KEY</command> removes a previously defined
+   column encryption key.  To be able to drop a column encryption key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column encryption key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the column encryption key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column encryption key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column encryption key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN ENCRYPTION KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN ENCRYPTION KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-encryption-key"/></member>
+   <member><xref linkend="sql-create-column-encryption-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/drop_column_master_key.sgml b/doc/src/sgml/ref/drop_column_master_key.sgml
new file mode 100644
index 000000000000..c85098ea1c99
--- /dev/null
+++ b/doc/src/sgml/ref/drop_column_master_key.sgml
@@ -0,0 +1,112 @@
+<!--
+doc/src/sgml/ref/drop_column_master_key.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-drop-column-master-key">
+ <indexterm zone="sql-drop-column-master-key">
+  <primary>DROP COLUMN MASTER KEY</primary>
+ </indexterm>
+
+ <refmeta>
+  <refentrytitle>DROP COLUMN MASTER KEY</refentrytitle>
+  <manvolnum>7</manvolnum>
+  <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+  <refname>DROP COLUMN MASTER KEY</refname>
+  <refpurpose>remove a column master key</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+DROP COLUMN MASTER KEY [ IF EXISTS ] <replaceable>name</replaceable> [ CASCADE | RESTRICT ]
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>DROP COLUMN MASTER KEY</command> removes a previously defined
+   column master key.  To be able to drop a column master key, you
+   must be its owner.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>Parameters</title>
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>IF EXISTS</literal></term>
+     <listitem>
+      <para>
+       Do not throw an error if the column master key does not exist.
+       A notice is issued in this case.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><replaceable>name</replaceable></term>
+
+     <listitem>
+      <para>
+       The name of the column master key.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CASCADE</literal></term>
+     <listitem>
+      <para>
+       Automatically drop objects that depend on the column master key,
+       and in turn all objects that depend on those objects
+       (see <xref linkend="ddl-depend"/>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>RESTRICT</literal></term>
+     <listitem>
+      <para>
+       Refuse to drop the column master key if any objects depend on it.  This
+       is the default.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+ </refsect1>
+
+ <refsect1>
+  <title>Examples</title>
+
+  <para>
+<programlisting>
+DROP COLUMN MASTER KEY cek1;
+</programlisting></para>
+ </refsect1>
+
+ <refsect1>
+  <title>Compatibility</title>
+
+  <para>
+   There is no <command>DROP COLUMN MASTER KEY</command> statement in
+   the SQL standard.
+  </para>
+ </refsect1>
+
+ <refsect1>
+  <title>See Also</title>
+
+    <simplelist type="inline">
+   <member><xref linkend="sql-alter-column-master-key"/></member>
+   <member><xref linkend="sql-create-column-master-key"/></member>
+  </simplelist>
+ </refsect1>
+
+</refentry>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 8b9d9f4cad43..7204971e1a12 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -691,6 +691,37 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  Note that a dump created
+        with this option cannot be restored into a database with column
+        encryption.
+       </para>
+       <!--
+           XXX: The latter would require another pg_dump option to put out
+           INSERT ... \gencr commands.
+       -->
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_dumpall.sgml b/doc/src/sgml/ref/pg_dumpall.sgml
index e62d05e5ab58..4bf60c729f7f 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -259,6 +259,33 @@ <title>Options</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--decrypt-encrypted-columns</option></term>
+      <listitem>
+       <para>
+        This option causes the values of all encrypted columns to be decrypted
+        and written to the output in plaintext.  By default, the values of
+        encrypted columns are written to the dump in ciphertext (that is, they
+        are not decrypted).
+       </para>
+
+       <para>
+        This option requires that <option>--inserts</option>,
+        <option>--column-inserts</option> or
+        <option>--rows-per-insert</option> is also specified.
+        (<command>COPY</command> does not support column decryption.)
+       </para>
+
+       <para>
+        For routine backups, the default behavior is appropriate and most
+        efficient.  This option is suitable if the data is meant to be
+        inspected or exported for other purposes.  Note that a dump created
+        with this option cannot be restored into a database with column
+        encryption.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--disable-dollar-quoting</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 9494f28063ad..cd61e99e86fe 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2255,6 +2255,28 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry>
+       <term><literal>\gencr</literal> [ <replaceable class="parameter">parameter</replaceable> ] ... </term>
+
+       <listitem>
+        <para>
+         Sends the current query buffer to the server for execution, as with
+         <literal>\g</literal>, with the specified parameters passed for any
+         parameter placeholders (<literal>$1</literal> etc.).  This command
+         ensures that any parameters corresponding to encrypted columns are
+         sent to the server encrypted.
+        </para>
+
+        <para>
+         Example:
+<programlisting>
+INSERT INTO tbl1 VALUES ($1, $2) \gencr 'first value' 'second value'
+</programlisting>
+        </para>
+       </listitem>
+      </varlistentry>
+
+
       <varlistentry>
         <term><literal>\getenv <replaceable class="parameter">psql_var</replaceable> <replaceable class="parameter">env_var</replaceable></literal></term>
 
@@ -3978,6 +4000,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>HIDE_COLUMN_ENCRYPTION</varname></term>
+        <listitem>
+        <para>
+         If this variable is set to <literal>true</literal>, column encryption
+         details are not displayed. This is mainly useful for regression
+         tests.
+        </para>
+        </listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index a3b743e8c1e7..e1425c222fab 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -36,6 +36,8 @@ <title>SQL Commands</title>
    &abort;
    &alterAggregate;
    &alterCollation;
+   &alterColumnEncryptionKey;
+   &alterColumnMasterKey;
    &alterConversion;
    &alterDatabase;
    &alterDefaultPrivileges;
@@ -90,6 +92,8 @@ <title>SQL Commands</title>
    &createAggregate;
    &createCast;
    &createCollation;
+   &createColumnEncryptionKey;
+   &createColumnMasterKey;
    &createConversion;
    &createDatabase;
    &createDomain;
@@ -137,6 +141,8 @@ <title>SQL Commands</title>
    &dropAggregate;
    &dropCast;
    &dropCollation;
+   &dropColumnEncryptionKey;
+   &dropColumnMasterKey;
    &dropConversion;
    &dropDatabase;
    &dropDomain;
diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index c99ae54cb026..86482eb4c81c 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -20,7 +20,9 @@
 
 #include "access/printsimple.h"
 #include "catalog/pg_type.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 
 /*
@@ -46,6 +48,11 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc)
 		pq_sendint16(&buf, attr->attlen);
 		pq_sendint32(&buf, attr->atttypmod);
 		pq_sendint16(&buf, 0);	/* format code */
+		if (MyProcPort->column_encryption_enabled)
+		{
+			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 d2f3b5728846..80ab5e875bd0 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,28 @@
  */
 #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/libpq-be.h"
 #include "libpq/pqformat.h"
+#include "miscadmin.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/inval.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 +166,139 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 	 */
 }
 
+/*
+ * Send ColumnMasterKey message, unless it's already been sent in this session
+ * for this key.
+ */
+List	   *cmk_sent = NIL;
+
+static void
+cmk_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+}
+
+static void
+MaybeSendColumnMasterKeyMessage(Oid cmkid)
+{
+	HeapTuple	tuple;
+	Form_pg_colmasterkey cmkform;
+	Datum		datum;
+	bool		isnull;
+	StringInfoData buf;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	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);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CMKOID, cmk_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	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.
+ */
+List	   *cek_sent = NIL;
+
+static void
+cek_change_cb(Datum arg, int cacheid, uint32 hashvalue)
+{
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
+void
+MaybeSendColumnEncryptionKeyMessage(Oid attcek)
+{
+	HeapTuple	tuple;
+	ScanKeyData skey[1];
+	SysScanDesc sd;
+	Relation	rel;
+	bool		found = false;
+	static bool registered_inval = false;
+	MemoryContext oldcontext;
+
+	Assert(MyProcPort->column_encryption_enabled);
+
+	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);
+
+	if (!registered_inval)
+	{
+		CacheRegisterSyscacheCallback(CEKDATAOID, cek_change_cb, (Datum) 0);
+		registered_inval = true;
+	}
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cek_sent = lappend_oid(cek_sent, attcek);
+	MemoryContextSwitchTo(oldcontext);
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -167,6 +315,7 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 						  List *targetlist, int16 *formats)
 {
 	int			natts = typeinfo->natts;
+	size_t		sz;
 	int			i;
 	ListCell   *tlist_item = list_head(targetlist);
 
@@ -183,14 +332,17 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 	 * Have to overestimate the size of the column-names, to account for
 	 * character set overhead.
 	 */
-	enlargeStringInfo(buf, (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
-							+ sizeof(Oid)	/* resorigtbl */
-							+ sizeof(AttrNumber)	/* resorigcol */
-							+ sizeof(Oid)	/* atttypid */
-							+ sizeof(int16) /* attlen */
-							+ sizeof(int32) /* attypmod */
-							+ sizeof(int16) /* format */
-							) * natts);
+	sz = (NAMEDATALEN * MAX_CONVERSION_GROWTH /* attname */
+		  + sizeof(Oid)	/* resorigtbl */
+		  + sizeof(AttrNumber)	/* resorigcol */
+		  + sizeof(Oid)	/* atttypid */
+		  + sizeof(int16) /* attlen */
+		  + sizeof(int32) /* attypmod */
+		  + sizeof(int16)); /* format */
+	if (MyProcPort->column_encryption_enabled)
+		sz += (sizeof(int32)		/* attcekid */
+			   + sizeof(int16));	/* attencalg */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +352,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 +385,22 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(atttypid))
+		{
+			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 +408,11 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		pq_writeint16(buf, att->attlen);
 		pq_writeint32(buf, atttypmod);
 		pq_writeint16(buf, format);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			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 d6fb261e2016..e3ecc64ea7a5 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 10bf26ce7c07..cea91d4a88a3 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 89a0221ec9b8..7e1a91b9749c 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/aclchk.c b/src/backend/catalog/aclchk.c
index aa5a2ed9483e..31978330c3f7 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -32,7 +32,9 @@
 #include "catalog/pg_am.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_conversion.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_default_acl.h"
@@ -3577,6 +3579,9 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEK:
+					case OBJECT_CEKDATA:
+					case OBJECT_CMK:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3607,6 +3612,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("must be owner of aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("must be owner of column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("must be owner of column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("must be owner of collation %s");
 						break;
@@ -3717,6 +3728,7 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEKDATA:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -5580,6 +5592,58 @@ pg_collation_ownercheck(Oid coll_oid, Oid roleid)
 	return has_privs_of_role(roleid, ownerId);
 }
 
+/*
+ * Ownership check for a column encryption key (specified by OID).
+ */
+bool
+pg_column_encryption_key_ownercheck(Oid cek_oid, Oid roleid)
+{
+	HeapTuple	tuple;
+	Oid			ownerId;
+
+	/* Superusers bypass all permission checking. */
+	if (superuser_arg(roleid))
+		return true;
+
+	tuple = SearchSysCache1(CEKOID, ObjectIdGetDatum(cek_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key with OID %u does not exist", cek_oid)));
+
+	ownerId = ((Form_pg_colenckey) GETSTRUCT(tuple))->cekowner;
+
+	ReleaseSysCache(tuple);
+
+	return has_privs_of_role(roleid, ownerId);
+}
+
+/*
+ * Ownership check for a column master key (specified by OID).
+ */
+bool
+pg_column_master_key_ownercheck(Oid cmk_oid, Oid roleid)
+{
+	HeapTuple	tuple;
+	Oid			ownerId;
+
+	/* Superusers bypass all permission checking. */
+	if (superuser_arg(roleid))
+		return true;
+
+	tuple = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmk_oid));
+	if (!HeapTupleIsValid(tuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key with OID %u does not exist", cmk_oid)));
+
+	ownerId = ((Form_pg_colmasterkey) GETSTRUCT(tuple))->cmkowner;
+
+	ReleaseSysCache(tuple);
+
+	return has_privs_of_role(roleid, ownerId);
+}
+
 /*
  * Ownership check for a conversion (specified by OID).
  */
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 39768fa22be1..6f1ea268f034 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -30,7 +30,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.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"
@@ -153,6 +156,9 @@ static const Oid object_classes[] = {
 	TypeRelationId,				/* OCLASS_TYPE */
 	CastRelationId,				/* OCLASS_CAST */
 	CollationRelationId,		/* OCLASS_COLLATION */
+	ColumnEncKeyRelationId,		/* OCLASS_CEK */
+	ColumnEncKeyDataRelationId,	/* OCLASS_CEKDATA */
+	ColumnMasterKeyRelationId,	/* OCLASS_CMK */
 	ConstraintRelationId,		/* OCLASS_CONSTRAINT */
 	ConversionRelationId,		/* OCLASS_CONVERSION */
 	AttrDefaultRelationId,		/* OCLASS_DEFAULT */
@@ -1487,6 +1493,9 @@ doDeletion(const ObjectAddress *object, int flags)
 
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONVERSION:
 		case OCLASS_LANGUAGE:
 		case OCLASS_OPCLASS:
@@ -2859,6 +2868,15 @@ getObjectClass(const ObjectAddress *object)
 		case CollationRelationId:
 			return OCLASS_COLLATION;
 
+		case ColumnEncKeyRelationId:
+			return OCLASS_CEK;
+
+		case ColumnEncKeyDataRelationId:
+			return OCLASS_CEKDATA;
+
+		case ColumnMasterKeyRelationId:
+			return OCLASS_CMK;
+
 		case ConstraintRelationId:
 			return OCLASS_CONSTRAINT;
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 9a80ccdccdf4..b65f194069e3 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -749,6 +749,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/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 284ca55469e0..62fdb6d70a70 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.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"
@@ -191,6 +194,48 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_COLLATION,
 		true
 	},
+	{
+		"column encryption key",
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		CEKOID,
+		CEKNAME,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		InvalidAttrNumber,
+		Anum_pg_colenckey_cekowner,
+		InvalidAttrNumber,
+		OBJECT_CEK,
+		true
+	},
+	{
+		"column encryption key data",
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		CEKDATAOID,
+		-1,
+		Anum_pg_colenckeydata_oid,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		InvalidAttrNumber,
+		-1,
+		false
+	},
+	{
+		"column master key",
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		CMKOID,
+		CMKNAME,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		InvalidAttrNumber,
+		Anum_pg_colmasterkey_cmkowner,
+		InvalidAttrNumber,
+		OBJECT_CMK,
+		true
+	},
 	{
 		"constraint",
 		ConstraintRelationId,
@@ -723,6 +768,18 @@ static const struct object_type_map
 	{
 		"collation", OBJECT_COLLATION
 	},
+	/* OCLASS_CEK */
+	{
+		"column encryption key", OBJECT_CEK
+	},
+	/* OCLASS_CEKDATA */
+	{
+		"column encryption key data", OBJECT_CEKDATA
+	},
+	/* OCLASS_CMK */
+	{
+		"column master key", OBJECT_CMK
+	},
 	/* OCLASS_CONSTRAINT */
 	{
 		"table constraint", OBJECT_TABCONSTRAINT
@@ -1028,6 +1085,8 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+			case OBJECT_CMK:
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1107,6 +1166,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					char	   *cekname = strVal(linitial(castNode(List, object)));
+					char	   *cmkname = strVal(lsecond(castNode(List, object)));
+					Oid			cekid;
+					Oid			cmkid;
+
+					cekid = get_cek_oid(cekname, missing_ok);
+					cmkid = get_cmk_oid(cmkname, missing_ok);
+
+					address.classId = ColumnEncKeyDataRelationId;
+					address.objectId = get_cekdata_oid(cekid, cmkid, missing_ok);
+					address.objectSubId = 0;
+				}
+				break;
 			case OBJECT_TRANSFORM:
 				{
 					TypeName   *typename = linitial_node(TypeName, castNode(List, object));
@@ -1294,6 +1368,16 @@ get_object_address_unqualified(ObjectType objtype,
 			address.objectId = get_am_oid(name, missing_ok);
 			address.objectSubId = 0;
 			break;
+		case OBJECT_CEK:
+			address.classId = ColumnEncKeyRelationId;
+			address.objectId = get_cek_oid(name, missing_ok);
+			address.objectSubId = 0;
+			break;
+		case OBJECT_CMK:
+			address.classId = ColumnMasterKeyRelationId;
+			address.objectId = get_cmk_oid(name, missing_ok);
+			address.objectSubId = 0;
+			break;
 		case OBJECT_DATABASE:
 			address.classId = DatabaseRelationId;
 			address.objectId = get_database_oid(name, missing_ok);
@@ -2254,6 +2338,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 	 */
 	switch (type)
 	{
+		case OBJECT_CEKDATA:
 		case OBJECT_PUBLICATION_NAMESPACE:
 		case OBJECT_USER_MAPPING:
 			if (list_length(name) != 1)
@@ -2328,6 +2413,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			objnode = (Node *) name;
 			break;
 		case OBJECT_ACCESS_METHOD:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_DATABASE:
 		case OBJECT_EVENT_TRIGGER:
 		case OBJECT_EXTENSION:
@@ -2358,6 +2445,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_PUBLICATION_REL:
 			objnode = (Node *) list_make2(name, linitial(args));
 			break;
+		case OBJECT_CEKDATA:
 		case OBJECT_PUBLICATION_NAMESPACE:
 		case OBJECT_USER_MAPPING:
 			objnode = (Node *) list_make2(linitial(name), linitial(args));
@@ -2635,6 +2723,16 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   NameListToString(castNode(List, object)));
 			break;
+		case OBJECT_CEK:
+			if (!pg_column_encryption_key_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+							   strVal(object));
+			break;
+		case OBJECT_CMK:
+			if (!pg_column_master_key_ownercheck(address.objectId, roleid))
+				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
+							   strVal(object));
+			break;
 		default:
 			elog(ERROR, "unrecognized object type: %d",
 				 (int) objtype);
@@ -3095,6 +3193,48 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				char	   *cekname = get_cek_name(object->objectId, missing_ok);
+
+				if (cekname)
+					appendStringInfo(&buffer, _("column encryption key %s"), cekname);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata cekdata;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+
+				cekdata = (Form_pg_colenckeydata) GETSTRUCT(tup);
+
+				appendStringInfo(&buffer, _("column encryption key %s data for master key %s"),
+								 get_cek_name(cekdata->ckdcekid, false),
+								 get_cmk_name(cekdata->ckdcmkid, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				char	   *cmkname = get_cmk_name(object->objectId, missing_ok);
+
+				if (cmkname)
+					appendStringInfo(&buffer, _("column master key %s"), cmkname);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4519,6 +4659,18 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "collation");
 			break;
 
+		case OCLASS_CEK:
+			appendStringInfoString(&buffer, "column encryption key");
+			break;
+
+		case OCLASS_CEKDATA:
+			appendStringInfoString(&buffer, "column encryption key data");
+			break;
+
+		case OCLASS_CMK:
+			appendStringInfoString(&buffer, "column master key");
+			break;
+
 		case OCLASS_CONSTRAINT:
 			getConstraintTypeDescription(&buffer, object->objectId,
 										 missing_ok);
@@ -4984,6 +5136,74 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+
+				tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckey) GETSTRUCT(tup);
+				appendStringInfoString(&buffer,
+									   quote_identifier(NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make1(pstrdup(NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CEKDATA:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata form;
+
+				tup = SearchSysCache1(CEKDATAOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column encryption key data %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colenckeydata) GETSTRUCT(tup);
+				appendStringInfo(&buffer,
+								 "of %s for %s", get_cek_name(form->ckdcekid, false), get_cmk_name(form->ckdcmkid, false));
+				if (objname)
+					*objname = list_make1(get_cek_name(form->ckdcekid, false));
+				if (objargs)
+					*objargs = list_make1(get_cmk_name(form->ckdcmkid, false));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+
+				tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(object->objectId));
+				if (!HeapTupleIsValid(tup))
+				{
+					if (!missing_ok)
+						elog(ERROR, "cache lookup failed for column master key %u",
+							 object->objectId);
+					break;
+				}
+				form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+				appendStringInfoString(&buffer,
+									   quote_identifier(NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make1(pstrdup(NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91c7..69f6175c60e0 100644
--- a/src/backend/commands/Makefile
+++ b/src/backend/commands/Makefile
@@ -19,6 +19,7 @@ OBJS = \
 	analyze.o \
 	async.o \
 	cluster.o \
+	colenccmds.o \
 	collationcmds.o \
 	comment.o \
 	constraint.o \
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 55219bb0974a..6caa21e325fb 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -22,7 +22,9 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_event_trigger.h"
 #include "catalog/pg_foreign_data_wrapper.h"
@@ -80,6 +82,12 @@ report_name_conflict(Oid classId, const char *name)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists");
+			break;
 		case EventTriggerRelationId:
 			msgfmt = gettext_noop("event trigger \"%s\" already exists");
 			break;
@@ -375,6 +383,8 @@ ExecRenameStmt(RenameStmt *stmt)
 			return RenameType(stmt);
 
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_EVENT_TRIGGER:
@@ -639,6 +649,9 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
 			break;
 
 		case OCLASS_CAST:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_DEFAULT:
 		case OCLASS_LANGUAGE:
@@ -872,6 +885,8 @@ ExecAlterOwnerStmt(AlterOwnerStmt *stmt)
 
 			/* Generic cases */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
diff --git a/src/backend/commands/colenccmds.c b/src/backend/commands/colenccmds.c
new file mode 100644
index 000000000000..bef8f00b9834
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,354 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/commands/colenccmds.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/table.h"
+#include "catalog/catalog.h"
+#include "catalog/dependency.h"
+#include "catalog/indexing.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+
+static void
+parse_cek_attributes(ParseState *pstate, List *definition, Oid *cmkoid_p, int16 *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = 0;
+	int16		alg;
+	char	   *encval;
+
+	Assert(cmkoid_p);
+
+	foreach(lc, definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "column_master_key") == 0)
+			defelp = &cmkEl;
+		else if (strcmp(defel->defname, "algorithm") == 0)
+			defelp = &algEl;
+		else if (strcmp(defel->defname, "encrypted_value") == 0)
+			defelp = &encvalEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column encryption key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (cmkEl)
+	{
+		char	   *val = defGetString(cmkEl);
+
+		cmkoid = GetSysCacheOid1(CMKNAME, Anum_pg_colmasterkey_oid, PointerGetDatum(val));
+		if (!cmkoid)
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_OBJECT),
+					errmsg("column master key \"%s\" does not exist", val));
+	}
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("attribute \"%s\" must be specified",
+						"column_master_key")));
+
+	if (algEl)
+	{
+		char	   *val = defGetString(algEl);
+
+		if (!alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"algorithm")));
+
+		if (strcmp(val, "RSAES_OAEP_SHA_1") == 0)
+			alg = PG_CMK_RSAES_OAEP_SHA_1;
+		else if (strcmp(val, "PG_CMK_RSAES_OAEP_SHA_256") == 0)
+			alg = PG_CMK_RSAES_OAEP_SHA_256;
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+		alg = PG_CMK_RSAES_OAEP_SHA_1;
+
+	if (encvalEl)
+	{
+		if (!encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must not be specified",
+							"encrypted_value")));
+
+		encval = defGetString(encvalEl);
+	}
+	else
+	{
+		if (encval_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"encrypted_value")));
+	}
+
+	*cmkoid_p = cmkoid;
+	if (alg_p)
+		*alg_p = alg;
+	if (encval_p)
+		*encval_p = encval;
+}
+
+static void
+insert_cekdata_record(Oid cekoid, Oid cmkoid, int16 alg, char *encval)
+{
+	Oid			cekdataoid;
+	Relation	rel;
+	Datum		values[Natts_pg_colenckeydata] = {0};
+	bool		nulls[Natts_pg_colenckeydata] = {0};
+	HeapTuple	tup;
+	ObjectAddress myself;
+	ObjectAddress other;
+
+	rel = table_open(ColumnEncKeyDataRelationId, RowExclusiveLock);
+
+	cekdataoid = GetNewOidWithIndex(rel, ColumnEncKeyDataOidIndexId, Anum_pg_colenckeydata_oid);
+	values[Anum_pg_colenckeydata_oid - 1] = ObjectIdGetDatum(cekdataoid);
+	values[Anum_pg_colenckeydata_ckdcekid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckeydata_ckdcmkid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colenckeydata_ckdcmkalg - 1] = Int16GetDatum(alg);
+	values[Anum_pg_colenckeydata_ckdencval - 1] = DirectFunctionCall1(byteain, CStringGetDatum(encval));
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyDataRelationId, cekdataoid);
+
+	/* dependency cekdata -> cek */
+	ObjectAddressSet(other, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_AUTO);
+
+	/* dependency cekdata -> cmk */
+	ObjectAddressSet(other, ColumnMasterKeyRelationId, cmkoid);
+	recordDependencyOn(&myself, &other, DEPENDENCY_NORMAL);
+
+	table_close(rel, NoLock);
+}
+
+ObjectAddress
+CreateCEK(ParseState *pstate, DefineStmt *stmt)
+{
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	aclresult = pg_database_aclcheck(MyDatabaseId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(MyDatabaseId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	cekoid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid,
+							 CStringGetDatum(strVal(llast(stmt->defnames))));
+	if (OidIsValid(cekoid))
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_OBJECT),
+				 errmsg("column encryption key \"%s\" already exists",
+						strVal(llast(stmt->defnames)))));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int16		alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] =
+		DirectFunctionCall1(namein, CStringGetDatum(strVal(llast(stmt->defnames))));
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyRelationId, cekoid);
+	recordDependencyOnOwner(ColumnEncKeyRelationId, cekoid, GetUserId());
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnEncKeyRelationId, cekoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt)
+{
+	Oid			cekoid;
+	ObjectAddress address;
+
+	cekoid = get_cek_oid(stmt->cekname, false);
+
+	if (!pg_column_encryption_key_ownercheck(cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, stmt->cekname);
+
+	if (stmt->isDrop)
+	{
+		Oid			cmkoid = 0;
+		Oid			cekdataoid;
+		ObjectAddress obj;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, NULL, NULL);
+		cekdataoid = get_cekdata_oid(cekoid, cmkoid, false);
+		ObjectAddressSet(obj, ColumnEncKeyDataRelationId, cekdataoid);
+		performDeletion(&obj, DROP_CASCADE, 0);
+		// TODO: prevent deleting all data entries for a key?
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int16		alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+		if (get_cekdata_oid(cekoid, cmkoid, true))
+			ereport(ERROR,
+					errcode(ERRCODE_DUPLICATE_OBJECT),
+					errmsg("column encryption key \"%s\" already has data for master key \"%s\"",
+						   stmt->cekname, get_cmk_name(cmkoid, false)));
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	InvokeObjectPostAlterHook(ColumnEncKeyRelationId, cekoid, 0);
+	ObjectAddressSet(address, ColumnEncKeyRelationId, cekoid);
+
+	return address;
+}
+
+ObjectAddress
+CreateCMK(ParseState *pstate, DefineStmt *stmt)
+{
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	aclresult = pg_database_aclcheck(MyDatabaseId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(MyDatabaseId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	cmkoid = GetSysCacheOid1(CMKNAME, Anum_pg_colmasterkey_oid,
+							 CStringGetDatum(strVal(llast(stmt->defnames))));
+	if (OidIsValid(cmkoid))
+		ereport(ERROR,
+				(errcode(ERRCODE_DUPLICATE_OBJECT),
+				 errmsg("column master key \"%s\" already exists",
+						strVal(llast(stmt->defnames)))));
+
+	foreach(lc, stmt->definition)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "realm") == 0)
+			defelp = &realmEl;
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("column master key attribute \"%s\" not recognized",
+							defel->defname),
+					 parser_errposition(pstate, defel->location)));
+		}
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (realmEl)
+		realm = defGetString(realmEl);
+	else
+		realm = "";
+
+	cmkoid = GetNewOidWithIndex(rel, ColumnMasterKeyOidIndexId, Anum_pg_colmasterkey_oid);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] =
+		DirectFunctionCall1(namein, CStringGetDatum(strVal(llast(stmt->defnames))));
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	recordDependencyOnOwner(ColumnMasterKeyRelationId, cmkoid, GetUserId());
+
+	ObjectAddressSet(myself, ColumnMasterKeyRelationId, cmkoid);
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	return myself;
+}
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 441f29d684ff..51b8686d4632 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,9 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -1027,6 +1030,9 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
 		case OCLASS_TYPE:
 		case OCLASS_CAST:
 		case OCLASS_COLLATION:
+		case OCLASS_CEK:
+		case OCLASS_CEKDATA:
+		case OCLASS_CMK:
 		case OCLASS_CONSTRAINT:
 		case OCLASS_CONVERSION:
 		case OCLASS_DEFAULT:
@@ -2056,6 +2062,9 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2139,6 +2148,9 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build
index 9b350d025ffc..6e26e158c449 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -5,6 +5,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index c4b54d054757..65dde324419e 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 = palloc0_array(Oid, nargs);
+		argorigcols = palloc0_array(AttrNumber, nargs);
 	}
 
 	/*
@@ -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,34 +691,49 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 		hash_seq_init(&hash_seq, prepared_queries);
 		while ((prep_stmt = hash_seq_search(&hash_seq)) != NULL)
 		{
+			int			num_params = prep_stmt->plansource->num_params;
 			TupleDesc	result_desc;
-			Datum		values[8];
-			bool		nulls[8] = {0};
+			Datum	   *tmp_ary;
+			Datum		values[10];
+			bool		nulls[10] = {0};
 
 			result_desc = prep_stmt->plansource->resultDesc;
 
 			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 = palloc_array(Datum, num_params);
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_types[i]);
+			values[3] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGTYPEOID));
+
+			tmp_ary = palloc_array(Datum, num_params);
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_origtbls[i]);
+			values[4] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGCLASSOID));
+
+			tmp_ary = palloc_array(Datum, num_params);
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = Int16GetDatum(prep_stmt->plansource->param_origcols[i]);
+			values[5] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, INT2OID));
+
 			if (result_desc)
 			{
-				Oid		   *result_types;
-
-				result_types = palloc_array(Oid, result_desc->natts);
+				tmp_ary = palloc_array(Datum, result_desc->natts);
 				for (int i = 0; i < result_desc->natts; i++)
-					result_types[i] = result_desc->attrs[i].atttypid;
-				values[4] = build_regtype_array(result_types, result_desc->natts);
+					tmp_ary[i] = ObjectIdGetDatum(result_desc->attrs[i].atttypid);
+				values[6] = PointerGetDatum(construct_array_builtin(tmp_ary, result_desc->natts, REGTYPEOID));
 			}
 			else
 			{
 				/* no result descriptor (for example, DML statement) */
-				nulls[4] = true;
+				nulls[6] = true;
 			}
-			values[5] = BoolGetDatum(prep_stmt->from_sql);
-			values[6] = Int64GetDatumFast(prep_stmt->plansource->num_generic_plans);
-			values[7] = Int64GetDatumFast(prep_stmt->plansource->num_custom_plans);
+
+			values[7] = BoolGetDatum(prep_stmt->from_sql);
+			values[8] = Int64GetDatumFast(prep_stmt->plansource->num_generic_plans);
+			values[9] = Int64GetDatumFast(prep_stmt->plansource->num_custom_plans);
 
 			tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
 								 values, nulls);
@@ -719,24 +742,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 = palloc_array(Datum, num_params);
-
-	for (i = 0; i < num_params; i++)
-		tmp_ary[i] = ObjectIdGetDatum(param_types[i]);
-
-	result = construct_array_builtin(tmp_ary, num_params, REGTYPEOID);
-	return PointerGetDatum(result);
-}
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ae19b98bb9a..07ad646a520c 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,9 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CEKDATA:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7d8a75d23c2d..e9b50e333a4f 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"
@@ -636,6 +637,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr);
 
 
 /* ----------------------------------------------------------------
@@ -935,6 +937,9 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+			GetColumnEncryption(colDef->encryption, attr);
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -6870,6 +6875,14 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attislocal = colDef->is_local;
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
+	if (colDef->encryption)
+		GetColumnEncryption(colDef->encryption, &attribute);
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attrealtypid = 0;
+		attribute.attencalg = 0;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12664,6 +12677,9 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 			case OCLASS_TYPE:
 			case OCLASS_CAST:
 			case OCLASS_COLLATION:
+			case OCLASS_CEK:
+			case OCLASS_CEKDATA:
+			case OCLASS_CMK:
 			case OCLASS_CONVERSION:
 			case OCLASS_LANGUAGE:
 			case OCLASS_LARGEOBJECT:
@@ -19299,3 +19315,83 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	char	   *cek = NULL;
+	Oid			cekoid;
+	bool		encdet = false;
+	int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+	foreach(lc, coldefencryption)
+	{
+		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;
+	attr->attencalg = alg;
+
+	/* override physical type */
+	if (encdet)
+		attr->atttypid = PG_ENCRYPTED_DETOID;
+	else
+		attr->atttypid = PG_ENCRYPTED_RNDOID;
+	get_typlenbyvalalign(attr->atttypid,
+						 &attr->attlen, &attr->attbyval, &attr->attalign);
+	attr->attstorage = get_typstorage(attr->atttypid);
+	attr->attcollation = InvalidOid;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index c795cb7a29ce..6cad27bf597b 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -25,6 +25,7 @@
 #include "access/xlogprefetcher.h"
 #include "catalog/pg_authid.h"
 #include "common/string.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "postmaster/postmaster.h"
@@ -706,7 +707,11 @@ check_client_encoding(char **newval, void **extra, GucSource source)
 	 */
 	if (PrepareClientEncoding(encoding) < 0)
 	{
-		if (IsTransactionState())
+		if (MyProcPort->column_encryption_enabled)
+			GUC_check_errdetail("Conversion between %s and %s is not possible when column encryption is enabled.",
+								canonical_name,
+								GetDatabaseEncodingName());
+		else if (IsTransactionState())
 		{
 			/* Must be a genuine no-such-conversion problem */
 			GUC_check_errcode(ERRCODE_FEATURE_NOT_SUPPORTED);
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 29bc26669b00..cde5b0e0870b 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/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 0a7b22f97e7b..a549b985d365 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4006,6 +4006,8 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 				if (WALK(coldef->compression))
 					return true;
+				if (WALK(coldef->encryption))
+					return true;
 				if (WALK(coldef->raw_default))
 					return true;
 				if (WALK(coldef->collClause))
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 6688c2a865b8..ce9b7a5f5f38 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 0d8d2928509c..190fb66aa91c 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,7 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 }
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
-		AlterEventTrigStmt AlterCollationStmt
+		AlterEventTrigStmt AlterCollationStmt AlterColumnEncryptionKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -420,6 +420,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <list>	parse_toplevel stmtmulti routine_body_stmt_list
 				OptTableElementList TableElementList OptInherit definition
+				list_of_definitions
 				OptTypedTableElementList TypedTableElementList
 				reloptions opt_reloptions
 				OptWith opt_definition func_args func_args_list
@@ -593,6 +594,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableConstraint TableLikeClause
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression column_storage opt_column_storage
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_match
@@ -691,8 +693,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P
 	DOUBLE_P DROP
 
-	EACH ELSE ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ESCAPE EVENT EXCEPT
-	EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
+	EACH ELSE ENABLE_P ENCODING ENCRYPTION ENCRYPTED END_P ENUM_P ESCAPE
+	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
@@ -715,7 +717,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
-	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+	MAPPING MASTER MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
 
 	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -943,6 +945,7 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -3681,14 +3684,15 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_storage opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_encryption opt_column_storage opt_column_compression create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 
 					n->colname = $1;
 					n->typeName = $2;
-					n->storage_name = $3;
-					n->compression = $4;
+					n->encryption = $3;
+					n->storage_name = $4;
+					n->compression = $5;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3697,8 +3701,8 @@ columnDef:	ColId Typename opt_column_storage opt_column_compression create_gener
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $5;
-					SplitColQualList($6, &n->constraints, &n->collClause,
+					n->fdwoptions = $6;
+					SplitColQualList($7, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *) n;
@@ -3755,6 +3759,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 		;
@@ -6250,6 +6259,24 @@ DefineStmt:
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+			| CREATE COLUMN ENCRYPTION KEY any_name WITH VALUES list_of_definitions
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CEK;
+					n->defnames = $5;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| CREATE COLUMN MASTER KEY any_name WITH definition
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = $7;
+					$$ = (Node *) n;
+				}
 		;
 
 definition: '(' def_list ')'						{ $$ = $2; }
@@ -6269,6 +6296,10 @@ def_elem:	ColLabel '=' def_arg
 				}
 		;
 
+list_of_definitions: definition						{ $$ = list_make1($1); }
+			| list_of_definitions ',' definition	{ $$ = lappend($1, $3); }
+		;
+
 /* Note: any simple identifier will be returned as a type name! */
 def_arg:	func_type						{ $$ = (Node *) $1; }
 			| reserved_keyword				{ $$ = (Node *) makeString(pstrdup($1)); }
@@ -6804,6 +6835,8 @@ object_type_name:
 
 drop_type_name:
 			ACCESS METHOD							{ $$ = OBJECT_ACCESS_METHOD; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| EVENT TRIGGER							{ $$ = OBJECT_EVENT_TRIGGER; }
 			| EXTENSION								{ $$ = OBJECT_EXTENSION; }
 			| FOREIGN DATA_P WRAPPER				{ $$ = OBJECT_FDW; }
@@ -9120,6 +9153,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) makeString($5);
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) makeString($5);
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -10128,6 +10181,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) makeString($5);
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) makeString($5);
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11284,6 +11355,34 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY name ADD_P VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = false;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN ENCRYPTION KEY name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16719,6 +16818,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16782,6 +16882,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17264,6 +17365,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17352,6 +17454,7 @@ bare_label_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index f668abfcb336..d0bec0a06d19 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));
+		{
+			*parstate->paramTypes = repalloc_array(*parstate->paramTypes,
+												   Oid, paramno);
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc_array(*parstate->paramOrigTbls,
+														  Oid, paramno);
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc_array(*parstate->paramOrigCols,
+														  AttrNumber, paramno);
+		}
 		else
-			*parstate->paramTypes = (Oid *) palloc(paramno * sizeof(Oid));
+		{
+			*parstate->paramTypes = palloc_array(Oid, paramno);
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc_array(Oid, paramno);
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc_array(AttrNumber, paramno);
+		}
 		/* 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 bd8057bc3e74..37406dd73d1f 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -594,6 +594,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/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 383bc4776ef9..512bc92b2964 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2254,12 +2254,27 @@ ProcessStartupPacket(Port *port, bool ssl_done, bool gss_done)
 									valptr),
 							 errhint("Valid values are: \"false\", 0, \"true\", 1, \"database\".")));
 			}
+			else if (strcmp(nameptr, "_pq_.column_encryption") == 0)
+			{
+				/*
+				 * Right now, the only accepted value is "1".  This gives room
+				 * to expand this into a version number, for example.
+				 */
+				if (strcmp(valptr, "1") == 0)
+					port->column_encryption_enabled = true;
+				else
+					ereport(FATAL,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for parameter \"%s\": \"%s\"",
+									"column_encryption",
+									valptr),
+							 errhint("Valid values are: 1.")));
+			}
 			else if (strncmp(nameptr, "_pq_.", 5) == 0)
 			{
 				/*
 				 * Any option beginning with _pq_. is reserved for use as a
-				 * protocol-level option, but at present no such options are
-				 * defined.
+				 * protocol-level option.
 				 */
 				unrecognized_protocol_options =
 					lappend(unrecognized_protocol_options, pstrdup(nameptr));
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 5352d5f4c6bb..af9e0e39e775 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -72,6 +72,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"
 
@@ -678,6 +679,8 @@ pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 								 const char *query_string,
 								 Oid **paramTypes,
 								 int *numParams,
+								 Oid **paramOrigTbls,
+								 AttrNumber **paramOrigCols,
 								 QueryEnvironment *queryEnv)
 {
 	Query	   *query;
@@ -692,7 +695,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.
@@ -1366,6 +1369,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_array(Oid, numParams);
+	AttrNumber *paramOrigCols = palloc_array(AttrNumber, numParams);
 
 	/*
 	 * Report query to various monitoring facilities.
@@ -1486,6 +1491,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 														  query_string,
 														  &paramTypes,
 														  &numParams,
+														  &paramOrigTbls,
+														  &paramOrigCols,
 														  NULL);
 
 		/* Done with the snapshot used for parsing */
@@ -1516,6 +1523,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 */
@@ -1813,6 +1822,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (type_is_encrypted(ptype))
+			{
+				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;
@@ -2603,8 +2622,44 @@ 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 (MyProcPort->column_encryption_enabled && type_is_encrypted(ptype))
+		{
+			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);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			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/tcop/utility.c b/src/backend/tcop/utility.c
index aa0081578788..549d31e5d4bf 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -30,6 +30,7 @@
 #include "commands/alter.h"
 #include "commands/async.h"
 #include "commands/cluster.h"
+#include "commands/colenccmds.h"
 #include "commands/collationcmds.h"
 #include "commands/comment.h"
 #include "commands/conversioncmds.h"
@@ -137,6 +138,7 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1443,14 @@ ProcessUtilitySlow(ParseState *pstate,
 															stmt->definition,
 															&secondaryObject);
 							break;
+						case OBJECT_CEK:
+							Assert(stmt->args == NIL);
+							address = CreateCEK(pstate, stmt);
+							break;
+						case OBJECT_CMK:
+							Assert(stmt->args == NIL);
+							address = CreateCMK(pstate, stmt);
+							break;
 						case OBJECT_COLLATION:
 							Assert(stmt->args == NIL);
 							address = DefineCollation(pstate,
@@ -1903,6 +1913,10 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2239,12 @@ AlterObjectTypeCommandTag(ObjectType objtype)
 		case OBJECT_COLUMN:
 			tag = CMDTAG_ALTER_TABLE;
 			break;
+		case OBJECT_CEK:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+		case OBJECT_CMK:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
 		case OBJECT_CONVERSION:
 			tag = CMDTAG_ALTER_CONVERSION;
 			break;
@@ -2640,6 +2660,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_STATISTIC_EXT:
 					tag = CMDTAG_DROP_STATISTICS;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_DROP_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_DROP_COLUMN_MASTER_KEY;
+					break;
 				default:
 					tag = CMDTAG_UNKNOWN;
 			}
@@ -2760,6 +2786,12 @@ CreateCommandTag(Node *parsetree)
 				case OBJECT_COLLATION:
 					tag = CMDTAG_CREATE_COLLATION;
 					break;
+				case OBJECT_CEK:
+					tag = CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY;
+					break;
+				case OBJECT_CMK:
+					tag = CMDTAG_CREATE_COLUMN_MASTER_KEY;
+					break;
 				case OBJECT_ACCESS_METHOD:
 					tag = CMDTAG_CREATE_ACCESS_METHOD;
 					break;
@@ -3063,6 +3095,10 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3217,7 +3253,7 @@ CreateCommandTag(Node *parsetree)
 			break;
 
 		default:
-			elog(WARNING, "unrecognized node type: %d",
+			elog(ERROR, "unrecognized node type: %d",
 				 (int) nodeTag(parsetree));
 			tag = CMDTAG_UNKNOWN;
 			break;
@@ -3688,6 +3724,10 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/arrayfuncs.c b/src/backend/utils/adt/arrayfuncs.c
index 495e449a9e9a..405f08a9b378 100644
--- a/src/backend/utils/adt/arrayfuncs.c
+++ b/src/backend/utils/adt/arrayfuncs.c
@@ -3386,6 +3386,7 @@ construct_array_builtin(Datum *elems, int nelems, Oid elmtype)
 			break;
 
 		case OIDOID:
+		case REGCLASSOID:
 		case REGTYPEOID:
 			elmlen = sizeof(Oid);
 			elmbyval = true;
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index a16a63f4957b..d5ea1689e29d 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -24,7 +24,10 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_namespace.h"
@@ -2658,6 +2661,25 @@ type_is_multirange(Oid typid)
 	return (get_typtype(typid) == TYPTYPE_MULTIRANGE);
 }
 
+bool
+type_is_encrypted(Oid typid)
+{
+	HeapTuple	tp;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+		bool		result;
+
+		result = (typtup->typcategory == TYPCATEGORY_ENCRYPTED);
+		ReleaseSysCache(tp);
+		return result;
+	}
+	else
+		return false;
+}
+
 /*
  * get_type_category_preferred
  *
@@ -3679,3 +3701,92 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+Oid
+get_cek_oid(const char *cekname, bool missing_ok)
+{
+	Oid			oid;
+
+	oid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid,
+						  CStringGetDatum(cekname));
+	if (!OidIsValid(oid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist", cekname)));
+	return oid;
+}
+
+char *
+get_cek_name(Oid cekid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cekname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+		return NULL;
+	}
+
+	cekname = pstrdup(NameStr(((Form_pg_colenckey) GETSTRUCT(tup))->cekname));
+
+	ReleaseSysCache(tup);
+
+	return cekname;
+}
+
+Oid
+get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok)
+{
+	Oid			cekdataid;
+
+	cekdataid = GetSysCacheOid2(CEKDATACEKCMK, Anum_pg_colenckeydata_oid,
+								ObjectIdGetDatum(cekid),
+								ObjectIdGetDatum(cmkid));
+	if (!OidIsValid(cekdataid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" has no data for master key \"%s\"",
+						get_cek_name(cekid, false), get_cmk_name(cmkid, false))));
+
+	return cekdataid;
+}
+
+Oid
+get_cmk_oid(const char *cmkname, bool missing_ok)
+{
+	Oid			oid;
+
+	oid = GetSysCacheOid1(CMKNAME, Anum_pg_colmasterkey_oid,
+						  CStringGetDatum(cmkname));
+	if (!OidIsValid(oid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist", cmkname)));
+	return oid;
+}
+
+char *
+get_cmk_name(Oid cmkid, bool missing_ok)
+{
+	HeapTuple	tup;
+	char	   *cmkname;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+
+	if (!HeapTupleIsValid(tup))
+	{
+		if (!missing_ok)
+			elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+		return NULL;
+	}
+
+	cmkname = pstrdup(NameStr(((Form_pg_colmasterkey) GETSTRUCT(tup))->cmkname));
+
+	ReleaseSysCache(tup);
+
+	return cmkname;
+}
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 0d6a29567487..f2a5e029e3a6 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,
@@ -417,8 +419,16 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 
 	if (num_params > 0)
 	{
-		plansource->param_types = (Oid *) palloc(num_params * sizeof(Oid));
+		plansource->param_types = palloc_array(Oid, num_params);
 		memcpy(plansource->param_types, param_types, num_params * sizeof(Oid));
+
+		plansource->param_origtbls = palloc0_array(Oid, num_params);
+		if (param_origtbls)
+			memcpy(plansource->param_origtbls, param_origtbls, num_params * sizeof(Oid));
+
+		plansource->param_origcols = palloc0_array(AttrNumber, num_params);
+		if (param_origcols)
+			memcpy(plansource->param_origcols, param_origcols, num_params * sizeof(AttrNumber));
 	}
 	else
 		plansource->param_types = NULL;
@@ -1535,13 +1545,22 @@ CopyCachedPlan(CachedPlanSource *plansource)
 	newsource->commandTag = plansource->commandTag;
 	if (plansource->num_params > 0)
 	{
-		newsource->param_types = (Oid *)
-			palloc(plansource->num_params * sizeof(Oid));
+		newsource->param_types = palloc_array(Oid, plansource->num_params);
 		memcpy(newsource->param_types, plansource->param_types,
 			   plansource->num_params * sizeof(Oid));
+		if (plansource->param_origtbls)
+		{
+			newsource->param_origtbls = palloc_array(Oid, plansource->num_params);
+			memcpy(newsource->param_origtbls, plansource->param_origtbls,
+				   plansource->num_params * sizeof(Oid));
+		}
+		if (plansource->param_origcols)
+		{
+			newsource->param_origcols = palloc_array(AttrNumber, plansource->num_params);
+			memcpy(newsource->param_origcols, plansource->param_origcols,
+				   plansource->num_params * sizeof(AttrNumber));
+		}
 	}
-	else
-		newsource->param_types = NULL;
 	newsource->num_params = plansource->num_params;
 	newsource->parserSetup = plansource->parserSetup;
 	newsource->parserSetupArg = plansource->parserSetupArg;
@@ -1549,8 +1568,6 @@ CopyCachedPlan(CachedPlanSource *plansource)
 	newsource->fixed_result = plansource->fixed_result;
 	if (plansource->resultDesc)
 		newsource->resultDesc = CreateTupleDescCopy(plansource->resultDesc);
-	else
-		newsource->resultDesc = NULL;
 	newsource->context = source_context;
 
 	querytree_context = AllocSetContextCreate(source_context,
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index eec644ec8489..6337f27bfcfd 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -29,7 +29,10 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.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 +270,43 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		256
 	},
+	{
+		ColumnEncKeyDataRelationId, /* CEKDATACEKCMK */
+		ColumnEncKeyCekidCmkidIndexId,
+		2,
+		{
+			Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid,
+		},
+		8
+	},
+	{
+		ColumnEncKeyDataRelationId, /* CEKDATAOID */
+		ColumnEncKeyDataOidIndexId,
+		1,
+		{
+			Anum_pg_colenckeydata_oid,
+		},
+		8
+	},
+	{
+		ColumnEncKeyRelationId, /* CEKNAME */
+		ColumnEncKeyNameIndexId,
+		1,
+		{
+			Anum_pg_colenckey_cekname,
+		},
+		8
+	},
+	{
+		ColumnEncKeyRelationId, /* CEKOID */
+		ColumnEncKeyOidIndexId,
+		1,
+		{
+			Anum_pg_colenckey_oid,
+		},
+		8
+	},
 	{OperatorClassRelationId,	/* CLAAMNAMENSP */
 		OpclassAmNameNspIndexId,
 		3,
@@ -289,6 +329,24 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
+	{
+		ColumnMasterKeyRelationId, /* CMKNAME */
+		ColumnMasterKeyNameIndexId,
+		1,
+		{
+			Anum_pg_colmasterkey_cmkname,
+		},
+		8
+	},
+	{
+		ColumnMasterKeyRelationId,	/* CMKOID */
+		ColumnMasterKeyOidIndexId,
+		1,
+		{
+			Anum_pg_colmasterkey_oid,
+		},
+		8
+	},
 	{CollationRelationId,		/* COLLNAMEENCNSP */
 		CollationNameEncNspIndexId,
 		3,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 0543c574c67e..de837ec3cddd 100644
--- a/src/backend/utils/mb/mbutils.c
+++ b/src/backend/utils/mb/mbutils.c
@@ -36,7 +36,9 @@
 
 #include "access/xact.h"
 #include "catalog/namespace.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
@@ -129,6 +131,12 @@ PrepareClientEncoding(int encoding)
 		encoding == PG_SQL_ASCII)
 		return 0;
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	if (IsTransactionState())
 	{
 		/*
@@ -236,6 +244,12 @@ SetClientEncoding(int encoding)
 		return 0;
 	}
 
+	/*
+	 * Cannot do conversion when column encryption is enabled.
+	 */
+	if (MyProcPort->column_encryption_enabled)
+		return -1;
+
 	/*
 	 * Search the cache for the entry previously prepared by
 	 * PrepareClientEncoding; if there isn't one, we lose.  While at it,
@@ -296,7 +310,9 @@ InitializeClientEncoding(void)
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("conversion between %s and %s is not supported",
 						pg_enc2name_tbl[pending_client_encoding].name,
-						GetDatabaseEncodingName())));
+						GetDatabaseEncodingName()),
+				 (MyProcPort->column_encryption_enabled) ?
+				 errdetail("Encoding conversion is not possible when column encryption is enabled.") : 0));
 	}
 
 	/*
diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c
index 44fa52cc55fe..89feceb3bde1 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -201,6 +201,12 @@ getSchemaData(Archive *fout, int *numTablesPtr)
 	pg_log_info("reading user-defined collations");
 	(void) getCollations(fout, &numCollations);
 
+	pg_log_info("reading column master keys");
+	getColumnMasterKeys(fout);
+
+	pg_log_info("reading column encryption keys");
+	getColumnEncryptionKeys(fout);
+
 	pg_log_info("reading user-defined conversions");
 	getConversions(fout, &numConversions);
 
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index e8b78982971e..f89daaa23171 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -84,6 +84,7 @@ typedef struct _connParams
 	char	   *pghost;
 	char	   *username;
 	trivalue	promptPassword;
+	int			column_encryption;
 	/* If not NULL, this overrides the dbname obtained from command line */
 	/* (but *only* the DB name, not anything else in the connstring) */
 	char	   *override_dbname;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 233198afc0c9..8a2bd7fb95ab 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3411,6 +3411,8 @@ _getObjectDescription(PQExpBuffer buf, TocEntry *te)
 
 	/* objects that don't require special decoration */
 	if (strcmp(type, "COLLATION") == 0 ||
+		strcmp(type, "COLUMN ENCRYPTION KEY") == 0 ||
+		strcmp(type, "COLUMN MASTER KEY") == 0 ||
 		strcmp(type, "CONVERSION") == 0 ||
 		strcmp(type, "DOMAIN") == 0 ||
 		strcmp(type, "TABLE") == 0 ||
@@ -3588,6 +3590,8 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, bool isData)
 		if (strcmp(te->desc, "AGGREGATE") == 0 ||
 			strcmp(te->desc, "BLOB") == 0 ||
 			strcmp(te->desc, "COLLATION") == 0 ||
+			strcmp(te->desc, "COLUMN ENCRYPTION KEY") == 0 ||
+			strcmp(te->desc, "COLUMN MASTER KEY") == 0 ||
 			strcmp(te->desc, "CONVERSION") == 0 ||
 			strcmp(te->desc, "DATABASE") == 0 ||
 			strcmp(te->desc, "DOMAIN") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index 28baa68fd4e9..960c3f39a91c 100644
--- a/src/bin/pg_dump/pg_backup_db.c
+++ b/src/bin/pg_dump/pg_backup_db.c
@@ -133,8 +133,8 @@ ConnectDatabase(Archive *AHX,
 	 */
 	do
 	{
-		const char *keywords[8];
-		const char *values[8];
+		const char *keywords[9];
+		const char *values[9];
 		int			i = 0;
 
 		/*
@@ -159,6 +159,11 @@ ConnectDatabase(Archive *AHX,
 		}
 		keywords[i] = "fallback_application_name";
 		values[i++] = progname;
+		if (cparams->column_encryption)
+		{
+			keywords[i] = "column_encryption";
+			values[i++] = "1";
+		}
 		keywords[i] = NULL;
 		values[i++] = NULL;
 		Assert(i <= lengthof(keywords));
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f8c4cb8d183d..478872801f1e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -47,6 +47,7 @@
 #include "catalog/pg_authid_d.h"
 #include "catalog/pg_cast_d.h"
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_default_acl_d.h"
 #include "catalog/pg_largeobject_d.h"
 #include "catalog/pg_largeobject_metadata_d.h"
@@ -225,6 +226,8 @@ static void dumpAccessMethod(Archive *fout, const AccessMethodInfo *aminfo);
 static void dumpOpclass(Archive *fout, const OpclassInfo *opcinfo);
 static void dumpOpfamily(Archive *fout, const OpfamilyInfo *opfinfo);
 static void dumpCollation(Archive *fout, const CollInfo *collinfo);
+static void dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo);
+static void dumpColumnMasterKey(Archive *fout, const CmkInfo *cekinfo);
 static void dumpConversion(Archive *fout, const ConvInfo *convinfo);
 static void dumpRule(Archive *fout, const RuleInfo *rinfo);
 static void dumpAgg(Archive *fout, const AggInfo *agginfo);
@@ -384,6 +387,7 @@ main(int argc, char **argv)
 		{"attribute-inserts", no_argument, &dopt.column_inserts, 1},
 		{"binary-upgrade", no_argument, &dopt.binary_upgrade, 1},
 		{"column-inserts", no_argument, &dopt.column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &dopt.cparams.column_encryption, 1},
 		{"disable-dollar-quoting", no_argument, &dopt.disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &dopt.disable_triggers, 1},
 		{"enable-row-security", no_argument, &dopt.enable_row_security, 1},
@@ -676,6 +680,9 @@ main(int argc, char **argv)
 	 * --inserts are already implied above if --column-inserts or
 	 * --rows-per-insert were specified.
 	 */
+	if (dopt.cparams.column_encryption && dopt.dump_inserts == 0)
+		pg_fatal("option --decrypt_encrypted_columns requires option --inserts, --rows-per-insert, or --column-inserts");
+
 	if (dopt.do_nothing && dopt.dump_inserts == 0)
 		pg_fatal("option --on-conflict-do-nothing requires option --inserts, --rows-per-insert, or --column-inserts");
 
@@ -1021,6 +1028,7 @@ help(const char *progname)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --enable-row-security        enable row security (dump only content user has\n"
@@ -5517,6 +5525,138 @@ getCollations(Archive *fout, int *numCollations)
 	return collinfo;
 }
 
+/*
+ * getColumnEncryptionKeys
+ *	  get information about column encryption keys
+ */
+void
+getColumnEncryptionKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CekInfo	   *cekinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cekname;
+	int			i_cekowner;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname,\n"
+					  " cek.cekowner\n"
+					  "FROM pg_colenckey cek");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cekname = PQfnumber(res, "cekname");
+	i_cekowner = PQfnumber(res, "cekowner");
+
+	cekinfo = pg_malloc(ntups * sizeof(CekInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		PGresult	   *res2;
+		int				ntups2;
+
+		cekinfo[i].dobj.objType = DO_CEK;
+		cekinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cekinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cekinfo[i].dobj);
+		cekinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cekname));
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT (SELECT cmkname FROM pg_catalog.pg_colmasterkey WHERE pg_colmasterkey.oid = ckdcmkid) AS cekcmkname,\n"
+						  " ckdcmkalg, ckdencval\n"
+						  "FROM pg_catalog.pg_colenckeydata\n"
+						  "WHERE ckdcekid = %u", cekinfo[i].dobj.catId.oid);
+		res2 = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+		ntups2 = PQntuples(res2);
+		cekinfo[i].numdata = ntups2;
+		cekinfo[i].cekcmknames = pg_malloc(sizeof(char *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			cekinfo[i].cekcmknames[j] = pg_strdup(PQgetvalue(res2, j, PQfnumber(res2, "cekcmkname")));
+			cekinfo[i].cekcmkalgs[j] = atoi(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkalg")));
+			cekinfo[i].cekencvals[j] = pg_strdup(PQgetvalue(res2, j, PQfnumber(res2, "ckdencval")));
+		}
+		PQclear(res2);
+
+		selectDumpableObject(&(cekinfo[i].dobj), fout);
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
+/*
+ * getColumnMasterKeys
+ *	  get information about column master keys
+ */
+void
+getColumnMasterKeys(Archive *fout)
+{
+	PQExpBuffer query;
+	PGresult   *res;
+	int			ntups;
+	CmkInfo	   *cmkinfo;
+	int			i_tableoid;
+	int			i_oid;
+	int			i_cmkname;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname,\n"
+					  " cmk.cmkowner, cmk.cmkrealm\n"
+					  "FROM pg_colmasterkey cmk");
+
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+
+	ntups = PQntuples(res);
+
+	i_tableoid = PQfnumber(res, "tableoid");
+	i_oid = PQfnumber(res, "oid");
+	i_cmkname = PQfnumber(res, "cmkname");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+
+	cmkinfo = pg_malloc(ntups * sizeof(CmkInfo));
+
+	for (int i = 0; i < ntups; i++)
+	{
+		cmkinfo[i].dobj.objType = DO_CMK;
+		cmkinfo[i].dobj.catId.tableoid = atooid(PQgetvalue(res, i, i_tableoid));
+		cmkinfo[i].dobj.catId.oid = atooid(PQgetvalue(res, i, i_oid));
+		AssignDumpId(&cmkinfo[i].dobj);
+		cmkinfo[i].dobj.name = pg_strdup(PQgetvalue(res, i, i_cmkname));
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8107,6 +8247,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcekname;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8216,17 +8359,29 @@ 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");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBuffer(q,
+						  "(SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek) AS attcekname,\n"
+						  "CASE atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcekname,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\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);
@@ -8245,6 +8400,9 @@ 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_attencdet = PQfnumber(res, "attencdet");
+	i_attencalg = PQfnumber(res, "attencalg");
 	i_attisdropped = PQfnumber(res, "attisdropped");
 	i_attlen = PQfnumber(res, "attlen");
 	i_attalign = PQfnumber(res, "attalign");
@@ -8306,6 +8464,9 @@ 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->attencdet = (bool *) pg_malloc(numatts * sizeof(bool));
+		tbinfo->attencalg = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char));
@@ -8334,6 +8495,18 @@ 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;
+			if (!PQgetisnull(res, r, i_attencdet))
+				tbinfo->attencdet[j] = (PQgetvalue(res, r, i_attencdet)[0] == 't');
+			else
+				tbinfo->attencdet[j] = 0;
+			if (!PQgetisnull(res, r, i_attencalg))
+				tbinfo->attencalg[j] = atoi(PQgetvalue(res, r, i_attencalg));
+			else
+				tbinfo->attencalg[j] = 0;
 			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));
@@ -9858,6 +10031,12 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_OPFAMILY:
 			dumpOpfamily(fout, (const OpfamilyInfo *) dobj);
 			break;
+		case DO_CEK:
+			dumpColumnEncryptionKey(fout, (const CekInfo *) dobj);
+			break;
+		case DO_CMK:
+			dumpColumnMasterKey(fout, (const CmkInfo *) dobj);
+			break;
 		case DO_COLLATION:
 			dumpCollation(fout, (const CollInfo *) dobj);
 			break;
@@ -13251,6 +13430,153 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+static const char *
+get_encalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+
+		case PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256:
+			return "AEAD_AES_128_CBC_HMAC_SHA_256";
+		case PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384:
+			return "AEAD_AES_192_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384:
+			return "AEAD_AES_256_CBC_HMAC_SHA_384";
+		case PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512:
+			return "AEAD_AES_256_CBC_HMAC_SHA_512";
+
+		default:
+			return NULL;
+	}
+}
+
+/*
+ * dumpColumnEncryptionKey
+ *	  dump the definition of the given column encryption key
+ */
+static void
+dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcekname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcekname = pg_strdup(fmtId(cekinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN ENCRYPTION KEY %s;\n",
+					  qcekname);
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  qcekname);
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtId(cekinfo->cekcmknames[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_encalg_name(cekinfo->cekcmkalgs[i]));
+		appendPQExpBuffer(query, "encrypted_value = ");
+		appendStringLiteralAH(query, cekinfo->cekencvals[i], fout);
+
+		appendPQExpBuffer(query, ")");
+		if (i < cekinfo->numdata - 1)
+		appendPQExpBuffer(query, ", ");
+	}
+
+	appendPQExpBufferStr(query, ";\n");
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cekinfo->dobj.catId, cekinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cekinfo->dobj.name,
+								  .owner = cekinfo->rolname,
+								  .description = "COLUMN ENCRYPTION KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					NULL, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 NULL, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcekname);
+}
+
+/*
+ * dumpColumnMasterKey
+ *	  dump the definition of the given column master key
+ */
+static void
+dumpColumnMasterKey(Archive *fout, const CmkInfo *cmkinfo)
+{
+	DumpOptions *dopt = fout->dopt;
+	PQExpBuffer delq;
+	PQExpBuffer query;
+	char	   *qcmkname;
+
+	/* Do nothing in data-only dump */
+	if (dopt->dataOnly)
+		return;
+
+	delq = createPQExpBuffer();
+	query = createPQExpBuffer();
+
+	qcmkname = pg_strdup(fmtId(cmkinfo->dobj.name));
+
+	appendPQExpBuffer(delq, "DROP COLUMN MASTER KEY %s;\n",
+					  qcmkname);
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  qcmkname);
+
+	appendPQExpBuffer(query, "realm = ");
+	appendStringLiteralAH(query, cmkinfo->cmkrealm, fout);
+
+	appendPQExpBufferStr(query, ");\n");
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_DEFINITION)
+		ArchiveEntry(fout, cmkinfo->dobj.catId, cmkinfo->dobj.dumpId,
+					 ARCHIVE_OPTS(.tag = cmkinfo->dobj.name,
+								  .owner = cmkinfo->rolname,
+								  .description = "COLUMN MASTER KEY",
+								  .section = SECTION_PRE_DATA,
+								  .createStmt = query->data,
+								  .dropStmt = delq->data));
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
+		dumpComment(fout, "COLUMN MASTER KEY", qcmkname,
+					NULL, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 NULL, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15334,6 +15660,22 @@ 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]));
+						/*
+						 * To reduce output size, we don't print the default
+						 * of encryption_type, but we do print the default of
+						 * algorithm, since we might want to change to a new
+						 * default algorithm sometime in the future.
+						 */
+						if (tbinfo->attencdet[j])
+							appendPQExpBuffer(q, "encryption_type = deterministic, ");
+						appendPQExpBuffer(q, "algorithm = '%s')",
+										  get_encalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -17898,6 +18240,8 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 			case DO_ACCESS_METHOD:
 			case DO_OPCLASS:
 			case DO_OPFAMILY:
+			case DO_CEK:
+			case DO_CMK:
 			case DO_COLLATION:
 			case DO_CONVERSION:
 			case DO_TABLE:
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 427f5d45f65b..cbdbae903f33 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -47,6 +47,8 @@ typedef enum
 	DO_ACCESS_METHOD,
 	DO_OPCLASS,
 	DO_OPFAMILY,
+	DO_CEK,
+	DO_CMK,
 	DO_COLLATION,
 	DO_CONVERSION,
 	DO_TABLE,
@@ -333,6 +335,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	char	  **attcekname;
+	int		   *attencalg;
+	bool	   *attencdet;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
 	char	   *attalign;		/* attribute align, used by binary_upgrade */
 	bool	   *attislocal;		/* true if attr has local definition */
@@ -664,6 +669,30 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	char	  **cekcmknames;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -711,6 +740,8 @@ extern AccessMethodInfo *getAccessMethods(Archive *fout, int *numAccessMethods);
 extern OpclassInfo *getOpclasses(Archive *fout, int *numOpclasses);
 extern OpfamilyInfo *getOpfamilies(Archive *fout, int *numOpfamilies);
 extern CollInfo *getCollations(Archive *fout, int *numCollations);
+extern void getColumnEncryptionKeys(Archive *fout);
+extern void getColumnMasterKeys(Archive *fout);
 extern ConvInfo *getConversions(Archive *fout, int *numConversions);
 extern TableInfo *getTables(Archive *fout, int *numTables);
 extern void getOwnedSeqs(Archive *fout, TableInfo tblinfo[], int numTables);
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 5de3241eb496..e562a677ed6a 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -69,6 +69,8 @@ enum dbObjectTypePriorities
 	PRIO_TSTEMPLATE,
 	PRIO_TSDICT,
 	PRIO_TSCONFIG,
+	PRIO_CMK,
+	PRIO_CEK,
 	PRIO_FDW,
 	PRIO_FOREIGN_SERVER,
 	PRIO_TABLE,
@@ -111,6 +113,8 @@ static const int dbObjectTypePriority[] =
 	PRIO_ACCESS_METHOD,			/* DO_ACCESS_METHOD */
 	PRIO_OPFAMILY,				/* DO_OPCLASS */
 	PRIO_OPFAMILY,				/* DO_OPFAMILY */
+	PRIO_CEK,					/* DO_CEK */
+	PRIO_CMK,					/* DO_CMK */
 	PRIO_COLLATION,				/* DO_COLLATION */
 	PRIO_CONVERSION,			/* DO_CONVERSION */
 	PRIO_TABLE,					/* DO_TABLE */
@@ -1322,6 +1326,16 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "OPERATOR FAMILY %s  (ID %d OID %u)",
 					 obj->name, obj->dumpId, obj->catId.oid);
 			return;
+		case DO_CEK:
+			snprintf(buf, bufsize,
+					 "COLUMN ENCRYPTION KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
+		case DO_CMK:
+			snprintf(buf, bufsize,
+					 "COLUMN MASTER KEY (ID %d OID %u)",
+					 obj->dumpId, obj->catId.oid);
+			return;
 		case DO_COLLATION:
 			snprintf(buf, bufsize,
 					 "COLLATION %s  (ID %d OID %u)",
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 083012ca39d5..bfe76dd315d4 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -93,6 +93,7 @@ static bool dosync = true;
 
 static int	binary_upgrade = 0;
 static int	column_inserts = 0;
+static int	decrypt_encrypted_columns = 0;
 static int	disable_dollar_quoting = 0;
 static int	disable_triggers = 0;
 static int	if_exists = 0;
@@ -154,6 +155,7 @@ main(int argc, char *argv[])
 		{"attribute-inserts", no_argument, &column_inserts, 1},
 		{"binary-upgrade", no_argument, &binary_upgrade, 1},
 		{"column-inserts", no_argument, &column_inserts, 1},
+		{"decrypt-encrypted-columns", no_argument, &decrypt_encrypted_columns, 1},
 		{"disable-dollar-quoting", no_argument, &disable_dollar_quoting, 1},
 		{"disable-triggers", no_argument, &disable_triggers, 1},
 		{"exclude-database", required_argument, NULL, 6},
@@ -424,6 +426,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --binary-upgrade");
 	if (column_inserts)
 		appendPQExpBufferStr(pgdumpopts, " --column-inserts");
+	if (decrypt_encrypted_columns)
+		appendPQExpBufferStr(pgdumpopts, " --decrypt-encrypted-columns");
 	if (disable_dollar_quoting)
 		appendPQExpBufferStr(pgdumpopts, " --disable-dollar-quoting");
 	if (disable_triggers)
@@ -649,6 +653,7 @@ help(void)
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
+	printf(_("  --decrypt-encrypted-columns  decrypt encrypted columns in the output\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
 	printf(_("  --disable-triggers           disable triggers during data-only restore\n"));
 	printf(_("  --exclude-database=PATTERN   exclude databases whose name matches PATTERN\n"));
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index a869321cdfc3..c5aca652faeb 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -593,6 +593,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO .+;/m,
+		like   => { %full_runs, section_pre_data => 1, },
+		unlike => { no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, section_pre_data => 1, },
+		unlike => { no_owner => 1, },
+	},
+
 	'ALTER FOREIGN DATA WRAPPER dummy OWNER TO' => {
 		regexp => qr/^ALTER FOREIGN DATA WRAPPER dummy OWNER TO .+;/m,
 		like   => { %full_runs, section_pre_data => 1, },
@@ -1193,6 +1205,24 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1611,6 +1641,24 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (column_master_key = cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, section_pre_data => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3984,8 +4032,12 @@
 			next;
 		}
 
-		# Add terminating semicolon
-		$create_sql{$test_db} .= $tests{$test}->{create_sql} . ";";
+		# Normalize command ending: strip all line endings, add
+		# semicolon if missing, add two newlines.
+		my $create_sql = $tests{$test}->{create_sql};
+		chomp $create_sql;
+		$create_sql .= ';' unless substr($create_sql, -1) eq ';';
+		$create_sql{$test_db} .= $create_sql . "\n\n";
 	}
 }
 
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index a141146e706b..2306f4ca60b9 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)
@@ -1492,6 +1495,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 e611e3266d0c..d6846acf5f74 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,36 @@ 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 c645d66418a9..3fbfe5dc6865 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1532,7 +1532,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;
@@ -1548,6 +1548,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;
@@ -1846,7 +1847,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)
@@ -1860,7 +1861,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");
@@ -1911,6 +1912,15 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!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 ||
@@ -2034,6 +2044,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)
@@ -2126,6 +2138,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 f8ce1a07060d..9f4fce26fdfe 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 2399cffa3fba..636729df1d28 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 f5b9e268f200..0b812f332211 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 dbe89d7eb2c0..b8ec27df1cc4 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1169,6 +1169,16 @@ static const VersionedQuery Query_for_list_of_subscriptions[] = {
 	{0, NULL}
 };
 
+static const VersionedQuery Query_for_list_of_ceks[] = {
+	{160000, "SELECT cekname FROM pg_catalog.pg_colenckey WHERE cekname LIKE '%s'"},
+	{0, NULL}
+};
+
+static const VersionedQuery Query_for_list_of_cmks[] = {
+	{160000, "SELECT cmkname FROM pg_catalog.pg_colmasterkey WHERE cmkname LIKE '%s'"},
+	{0, NULL}
+};
+
 /*
  * This is a list of all "things" in Pgsql, which can show up after CREATE or
  * DROP; and there is also a query to get a list of them.
@@ -1202,6 +1212,8 @@ static const pgsql_thing_t words_after_create[] = {
 	{"CAST", NULL, NULL, NULL}, /* Casts have complex structures for names, so
 								 * skip it */
 	{"COLLATION", NULL, NULL, &Query_for_list_of_collations},
+	{"COLUMN ENCRYPTION KEY", NULL, NULL, NULL},
+	{"COLUMN MASTER KEY KEY", NULL, NULL, NULL},
 
 	/*
 	 * CREATE CONSTRAINT TRIGGER is not supported here because it is designed
@@ -1692,7 +1704,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",
@@ -1900,6 +1912,22 @@ psql_completion(const char *text, int start, int end)
 	else if (Matches("ALTER", "COLLATION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "REFRESH VERSION", "RENAME TO", "SET SCHEMA");
 
+	/* ALTER/DROP COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "ENCRYPTION", "KEY"))
+		COMPLETE_WITH_VERSIONED_QUERY(Query_for_list_of_ceks);
+
+	/* ALTER COLUMN ENCRYPTION KEY */
+	else if (Matches("ALTER", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("ADD VALUE (", "DROP VALUE (", "OWNER TO", "RENAME TO");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_VERSIONED_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("OWNER TO", "RENAME TO");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2794,6 +2822,26 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("true", "false");
 	}
 
+	/* CREATE/ALTER/DROP COLUMN ... KEY */
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN"))
+		COMPLETE_WITH("ENCRYPTION", "MASTER");
+	else if (Matches("CREATE|ALTER|DROP", "COLUMN", "ENCRYPTION|MASTER"))
+		COMPLETE_WITH("KEY");
+
+	/* CREATE COLUMN ENCRYPTION KEY */
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("VALUES");
+	else if (Matches("CREATE", "COLUMN", "ENCRYPTION", "KEY", MatchAny, "WITH", "VALUES"))
+		COMPLETE_WITH("(");
+
+	/* CREATE COLUMN MASTER KEY */
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("WITH");
+	else if (Matches("CREATE", "COLUMN", "MASTER", "KEY", MatchAny, "WITH"))
+		COMPLETE_WITH("(");
+
 	/* CREATE DATABASE */
 	else if (Matches("CREATE", "DATABASE", MatchAny))
 		COMPLETE_WITH("OWNER", "TEMPLATE", "ENCODING", "TABLESPACE",
@@ -3517,6 +3565,7 @@ psql_completion(const char *text, int start, int end)
 			 Matches("DROP", "ACCESS", "METHOD", MatchAny) ||
 			 (Matches("DROP", "AGGREGATE|FUNCTION|PROCEDURE|ROUTINE", MatchAny, MatchAny) &&
 			  ends_with(prev_wd, ')')) ||
+			 Matches("DROP", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
 			 Matches("DROP", "EVENT", "TRIGGER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 Matches("DROP", "FOREIGN", "TABLE", MatchAny) ||
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 971a74cf22b4..db1f3b8811a1 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/dependency.h b/src/include/catalog/dependency.h
index 98a1a8428907..1a2a8177d1a7 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -92,6 +92,9 @@ typedef enum ObjectClass
 	OCLASS_TYPE,				/* pg_type */
 	OCLASS_CAST,				/* pg_cast */
 	OCLASS_COLLATION,			/* pg_collation */
+	OCLASS_CEK,					/* pg_colenckey */
+	OCLASS_CEKDATA,				/* pg_colenckeydata */
+	OCLASS_CMK,					/* pg_colmasterkey */
 	OCLASS_CONSTRAINT,			/* pg_constraint */
 	OCLASS_CONVERSION,			/* pg_conversion */
 	OCLASS_DEFAULT,				/* pg_attrdef */
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 45ffa99692e9..f3a0b1cee9c0 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -63,6 +63,9 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_colmasterkey.h',
+  'pg_colenckey.h',
+  'pg_colenckeydata.h',
 ]
 
 bki_data = [
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 61cd5914304a..a0bd7081327c 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 4cc129bebd83..dddb27113fa1 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 053294c99f37..550a53649beb 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 4471eb6bbea4..878b0bcbbf50 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 000000000000..095ed712b03b
--- /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 000000000000..3e4dea72180c
--- /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 000000000000..0344cc420168
--- /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 dbcae7ffdd21..0ca401ffe478 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 bc5f8213f3af..4737a7f9ed17 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 b3b6a7e616a9..5343580b0632 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 a07e737a337e..7f6298452fb2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8033,9 +8033,9 @@
   proname => 'pg_prepared_statement', prorows => '1000', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'record',
   proargtypes => '',
-  proallargtypes => '{text,text,timestamptz,_regtype,_regtype,bool,int8,int8}',
-  proargmodes => '{o,o,o,o,o,o,o,o}',
-  proargnames => '{name,statement,prepare_time,parameter_types,result_types,from_sql,generic_plans,custom_plans}',
+  proallargtypes => '{text,text,timestamptz,_regtype,_regclass,_int2,_regtype,bool,int8,int8}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{name,statement,prepare_time,parameter_types,parameter_orig_tables,parameter_orig_columns,result_types,from_sql,generic_plans,custom_plans}',
   prosrc => 'pg_prepared_statement' },
 { oid => '2511', descr => 'get the open cursors for this session',
   proname => 'pg_cursor', prorows => '1000', proretset => 't',
@@ -11820,4 +11820,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 df4587946357..851063cf5f05 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 => 'b',
+  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 => 'b',
+  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 48a255913742..c1d30903b5b2 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -294,6 +294,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPCATEGORY_USER		'U'
 #define  TYPCATEGORY_BITSTRING	'V' /* er ... "varbit"? */
 #define  TYPCATEGORY_UNKNOWN	'X'
+#define  TYPCATEGORY_ENCRYPTED	'Y'
 #define  TYPCATEGORY_INTERNAL	'Z'
 
 #define  TYPALIGN_CHAR			'c' /* char alignment (i.e. unaligned) */
diff --git a/src/include/commands/colenccmds.h b/src/include/commands/colenccmds.h
new file mode 100644
index 000000000000..d9741e100bb2
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,25 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/commands/colenccmds.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COLENCCMDS_H
+#define COLENCCMDS_H
+
+#include "catalog/objectaddress.h"
+#include "parser/parse_node.h"
+
+extern ObjectAddress CreateCEK(ParseState *pstate, DefineStmt *stmt);
+extern ObjectAddress AlterColumnEncryptionKey(ParseState *pstate, AlterColumnEncryptionKeyStmt *stmt);
+extern ObjectAddress CreateCMK(ParseState *pstate, DefineStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 6d452ec6d956..e32c7779c955 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -164,6 +164,7 @@ typedef struct Port
 	 */
 	char	   *database_name;
 	char	   *user_name;
+	bool		column_encryption_enabled;
 	char	   *cmdline_options;
 	List	   *guc_options;
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index aead2afd6ef7..8bd834b21bba 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -686,6 +686,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? */
@@ -1864,6 +1865,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2056,6 +2060,19 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	char	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index 3d3a5918c2fc..6c2866d19f8a 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/kwlist.h b/src/include/parser/kwlist.h
index 9a7cc0c6bd1d..813cf3d23d31 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -149,6 +149,7 @@ PG_KEYWORD("else", ELSE, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enable", ENABLE_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encoding", ENCODING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("encrypted", ENCRYPTED, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("encryption", ENCRYPTION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("end", END_P, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("enum", ENUM_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("escape", ESCAPE, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -250,6 +251,7 @@ PG_KEYWORD("lock", LOCK_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("master", MASTER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 962ebf65de18..f6a7178766ef 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 df1ee660d831..da202b28a7c3 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/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f43..d82a2e1171ba 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -29,6 +29,8 @@ PG_CMDTAG(CMDTAG_ALTER_ACCESS_METHOD, "ALTER ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_AGGREGATE, "ALTER AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CAST, "ALTER CAST", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_COLLATION, "ALTER COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY, "ALTER COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_ALTER_COLUMN_MASTER_KEY, "ALTER COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONSTRAINT, "ALTER CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_CONVERSION, "ALTER CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_ALTER_DATABASE, "ALTER DATABASE", false, false, false)
@@ -86,6 +88,8 @@ PG_CMDTAG(CMDTAG_CREATE_ACCESS_METHOD, "CREATE ACCESS METHOD", true, false, fals
 PG_CMDTAG(CMDTAG_CREATE_AGGREGATE, "CREATE AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CAST, "CREATE CAST", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_COLLATION, "CREATE COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_ENCRYPTION_KEY, "CREATE COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_CREATE_COLUMN_MASTER_KEY, "CREATE COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONSTRAINT, "CREATE CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_CONVERSION, "CREATE CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_CREATE_DATABASE, "CREATE DATABASE", false, false, false)
@@ -138,6 +142,8 @@ PG_CMDTAG(CMDTAG_DROP_ACCESS_METHOD, "DROP ACCESS METHOD", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_AGGREGATE, "DROP AGGREGATE", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CAST, "DROP CAST", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_COLLATION, "DROP COLLATION", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_ENCRYPTION_KEY, "DROP COLUMN ENCRYPTION KEY", true, false, false)
+PG_CMDTAG(CMDTAG_DROP_COLUMN_MASTER_KEY, "DROP COLUMN MASTER KEY", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONSTRAINT, "DROP CONSTRAINT", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_CONVERSION, "DROP CONVERSION", true, false, false)
 PG_CMDTAG(CMDTAG_DROP_DATABASE, "DROP DATABASE", false, false, false)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 5d34978f329e..7502d71b0d20 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/acl.h b/src/include/utils/acl.h
index 9a4df3a5dacc..a7c51dc3c89a 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -318,6 +318,8 @@ extern bool pg_opclass_ownercheck(Oid opc_oid, Oid roleid);
 extern bool pg_opfamily_ownercheck(Oid opf_oid, Oid roleid);
 extern bool pg_database_ownercheck(Oid db_oid, Oid roleid);
 extern bool pg_collation_ownercheck(Oid coll_oid, Oid roleid);
+extern bool pg_column_encryption_key_ownercheck(Oid cek_oid, Oid roleid);
+extern bool pg_column_master_key_ownercheck(Oid cmk_oid, Oid roleid);
 extern bool pg_conversion_ownercheck(Oid conv_oid, Oid roleid);
 extern bool pg_ts_dict_ownercheck(Oid dict_oid, Oid roleid);
 extern bool pg_ts_config_ownercheck(Oid cfg_oid, Oid roleid);
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 50f028830525..7258d4007705 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -163,6 +163,7 @@ extern bool type_is_rowtype(Oid typid);
 extern bool type_is_enum(Oid typid);
 extern bool type_is_range(Oid typid);
 extern bool type_is_multirange(Oid typid);
+extern bool type_is_encrypted(Oid typid);
 extern void get_type_category_preferred(Oid typid,
 										char *typcategory,
 										bool *typispreferred);
@@ -202,6 +203,11 @@ extern Oid	get_publication_oid(const char *pubname, bool missing_ok);
 extern char *get_publication_name(Oid pubid, bool missing_ok);
 extern Oid	get_subscription_oid(const char *subname, bool missing_ok);
 extern char *get_subscription_name(Oid subid, bool missing_ok);
+extern Oid	get_cek_oid(const char *cekname, bool missing_ok);
+extern char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, bool missing_ok);
+extern Oid	get_cmk_oid(const char *cmkname, bool missing_ok);
+extern char *get_cmk_name(Oid cmkid, bool missing_ok);
 
 #define type_is_array(typid)  (get_element_type(typid) != InvalidOid)
 /* type_is_array_domain accepts both plain arrays and domains over arrays */
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 0499635f5945..9a7cf794cecf 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -101,6 +101,10 @@ 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 +203,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 4463ea66bea5..489c6ee734bb 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAME,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAME,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 1d31b256fc9e..4812f862ca6a 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 e8bcc8837091..fabad38ac0f1 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 746e9b4f1efc..f2a37390847e 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -341,6 +341,14 @@ 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)},
+
+	{"column_encryption", "PGCOLUMNENCRYPTION", "0", NULL,
+		"Column-Encryption", "", 1,
+	offsetof(struct pg_conn, column_encryption_setting)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -2454,6 +2462,7 @@ PQconnectPoll(PGconn *conn)
 #ifdef ENABLE_GSS
 		conn->try_gss = (conn->gssencmode[0] != 'd');	/* "disable" */
 #endif
+		conn->column_encryption_enabled = (conn->column_encryption_setting[0] == '1');
 
 		reset_connection_state_machine = false;
 		need_new_connection = true;
@@ -3249,7 +3258,7 @@ PQconnectPoll(PGconn *conn)
 				 * request or an error here.  Anything else probably means
 				 * it's not Postgres on the other end at all.
 				 */
-				if (!(beresp == 'R' || beresp == 'E'))
+				if (!(beresp == 'R' || beresp == 'E' || beresp == 'v'))
 				{
 					appendPQExpBuffer(&conn->errorMessage,
 									  libpq_gettext("expected authentication request from server, but received %c\n"),
@@ -3405,6 +3414,17 @@ PQconnectPoll(PGconn *conn)
 
 					goto error_return;
 				}
+				else if (beresp == 'v')
+				{
+					if (pqGetNegotiateProtocolVersion3(conn))
+					{
+						/* We'll come back when there is more data */
+						return PGRES_POLLING_READING;
+					}
+					/* OK, we read the message; mark data consumed */
+					conn->inStart = conn->inCursor;
+					goto error_return;
+				}
 
 				/* It is an authentication request. */
 				conn->auth_req_received = true;
@@ -4080,6 +4100,22 @@ freePGconn(PGconn *conn)
 	free(conn->krbsrvname);
 	free(conn->gsslib);
 	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 */
 	free(conn->write_err_msg);
 	free(conn->inBuffer);
diff --git a/src/interfaces/libpq/fe-encrypt-openssl.c b/src/interfaces/libpq/fe-encrypt-openssl.c
new file mode 100644
index 000000000000..974a3aa810af
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,766 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 <openssl/evp.h>
+
+
+#ifdef TEST_ENCRYPT
+
+/*
+ * Test data from
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5>
+ */
+
+/*
+ * 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;
+
+	if (encrlen < 0)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("encrypted value has invalid length"));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	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 of 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 of 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;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	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, md_value, &md_len))
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("digest signing failed: %s"),
+						  ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	Assert(md_len == md_hash_length(md));
+	memcpy(iv, md_value, ivlen);
+
+	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 000000000000..b0093dc52704
--- /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 0274c1b156c6..835e3516fff6 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,9 @@
 #include <unistd.h>
 #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);
@@ -1185,6 +1190,382 @@ 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");
+				if (!fp)
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not run command \"%s\": %m\n"), command);
+					free(command);
+					goto fail;
+				}
+				free(command);
+				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
@@ -1253,6 +1634,55 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 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;
@@ -1260,6 +1690,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;
@@ -1531,7 +1962,9 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   NULL,
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1649,6 +2082,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;
@@ -1676,7 +2123,9 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   paramForceColumnEncryptions,
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1777,7 +2226,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;
@@ -1824,14 +2275,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 = &paramDesc->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;
 		}
 	}
@@ -1850,6 +2340,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1868,9 +2359,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
 		{
@@ -2308,12 +2844,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);
 }
@@ -3612,6 +4163,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)
 {
@@ -3797,6 +4359,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 f001137b7692..418433ed6957 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -43,6 +43,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);
@@ -301,6 +303,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 &&
@@ -558,6 +576,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) ||
@@ -572,6 +592,21 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 2, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -593,8 +628,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;
 	}
 
@@ -696,10 +733,31 @@ 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;
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn))
+				goto not_enough_data;
+			if (pqGetInt(&cekalg, 2, conn))
+				goto not_enough_data;
+			if (pqGetInt(&flags, 2, conn))
+				goto not_enough_data;
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
 		result->paramDescs[i].typid = typid;
+		result->paramDescs[i].cekid = cekid;
+		result->paramDescs[i].cekalg = cekalg;
+		result->paramDescs[i].flags = flags;
 	}
 
 	/* Success! */
@@ -1397,6 +1455,40 @@ reportErrorPosition(PQExpBuffer msg, const char *query, int loc, int encoding)
 }
 
 
+int
+pqGetNegotiateProtocolVersion3(PGconn *conn)
+{
+	int			their_version;
+	int			num;
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+	if (pqGetInt(&their_version, 4, conn) != 0)
+		return EOF;
+	if (pqGetInt(&num, 4, conn) != 0)
+		return EOF;
+	for (int i = 0; i < num; i++)
+	{
+		if (pqGets(&conn->workBuffer, conn))
+			return EOF;
+		if (buf.len > 0)
+			appendPQExpBufferChar(&buf, ' ');
+		appendPQExpBufferStr(&buf, conn->workBuffer.data);
+	}
+
+	if (their_version != conn->pversion)
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("protocol version not supported by server: client uses %d.%d, server supports %d.%d"),
+						  PG_PROTOCOL_MAJOR(conn->pversion), PG_PROTOCOL_MINOR(conn->pversion),
+						  PG_PROTOCOL_MAJOR(their_version), PG_PROTOCOL_MINOR(their_version));
+	else
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("protocol extension not supported by server: %s\n"), buf.data);
+
+	termPQExpBuffer(&buf);
+	return 0;
+}
+
 /*
  * Attempt to read a ParameterStatus message.
  * This is possible in several places, so we break it out as a subroutine.
@@ -1425,6 +1517,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.
@@ -2250,6 +2425,9 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	if (conn->column_encryption_enabled)
+		ADD_STARTUP_OPTION("_pq_.column_encryption", "1");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index 5d68cf2eb3b5..8396888a5b16 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -450,7 +450,8 @@ pqTraceOutputS(FILE *f, const char *message, int *cursor)
 
 /* ParameterDescription */
 static void
-pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -458,12 +459,21 @@ 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);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt16(f, message, cursor);
+			pqTraceOutputInt16(f, message, cursor);
+		}
+	}
 }
 
 /* RowDescription */
 static void
-pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
+pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress,
+			   bool column_encryption_enabled)
 {
 	int			nfields;
 
@@ -479,6 +489,11 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
 		pqTraceOutputInt16(f, message, cursor);
 		pqTraceOutputInt32(f, message, cursor, false);
 		pqTraceOutputInt16(f, message, cursor);
+		if (column_encryption_enabled)
+		{
+			pqTraceOutputInt32(f, message, cursor, regress);
+			pqTraceOutputInt16(f, message, cursor);
+		}
 	}
 }
 
@@ -514,6 +529,30 @@ pqTraceOutputW(FILE *f, const char *message, int *cursor, int length)
 		pqTraceOutputInt16(f, message, cursor);
 }
 
+/* ColumnMasterKey */
+static void
+pqTraceOutputy(FILE *f, const char *message, int *cursor, bool regress)
+{
+	fprintf(f, "ColumnMasterKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputString(f, message, cursor, false);
+	pqTraceOutputString(f, message, cursor, false);
+}
+
+/* ColumnEncryptionKey */
+static void
+pqTraceOutputY(FILE *f, const char *message, int *cursor, bool regress)
+{
+	int			len;
+
+	fprintf(f, "ColumnEncryptionKey\t");
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt32(f, message, cursor, regress);
+	pqTraceOutputInt16(f, message, cursor);
+	len = pqTraceOutputInt32(f, message, cursor, false);
+	pqTraceOutputNchar(f, len, message, cursor);
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -647,10 +686,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 				fprintf(conn->Pfdebug, "Sync"); /* no message content */
 			break;
 		case 't':				/* Parameter Description */
-			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'T':				/* Row Description */
-			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case 'v':				/* Negotiate Protocol Version */
 			pqTraceOutputv(conn->Pfdebug, message, &logCursor);
@@ -665,6 +706,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			fprintf(conn->Pfdebug, "Terminate");
 			/* No message content */
 			break;
+		case 'y':
+			pqTraceOutputy(conn->Pfdebug, message, &logCursor, regress);
+			break;
+		case 'Y':
+			pqTraceOutputY(conn->Pfdebug, message, &logCursor, regress);
+			break;
 		case 'Z':				/* Ready For Query */
 			pqTraceOutputZ(conn->Pfdebug, message, &logCursor);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index b7df3224c0f7..0234724396d3 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 c75ed63a2c62..09307dab11c1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -112,6 +112,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -343,6 +346,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.
@@ -396,6 +419,8 @@ 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 */
+	char	   *column_encryption_setting;	/* column_encryption connection parameter (0 or 1) */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -477,6 +502,13 @@ struct pg_conn
 	PGVerbosity verbosity;		/* error/notice message verbosity */
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
+	bool		column_encryption_enabled;	/* parsed version of column_encryption_setting */
+
+	/* 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 */
@@ -673,6 +705,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);
@@ -685,6 +721,7 @@ extern void pqParseInput3(PGconn *conn);
 extern int	pqGetErrorNotice3(PGconn *conn, bool isError);
 extern void pqBuildErrorMessage3(PQExpBuffer msg, const PGresult *res,
 								 PGVerbosity verbosity, PGContextVisibility show_context);
+extern int	pqGetNegotiateProtocolVersion3(PGconn *conn);
 extern int	pqGetCopyData3(PGconn *conn, char **buffer, int async);
 extern int	pqGetline3(PGconn *conn, char *s, int maxlen);
 extern int	pqGetlineAsync3(PGconn *conn, char *buffer, int bufsize);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index bc047e00d620..ab69e982e262 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -23,6 +23,7 @@ endif
 
 if ssl.found()
   libpq_sources += files('fe-secure-common.c')
+  libpq_sources += files('fe-encrypt-openssl.c')
   libpq_sources += files('fe-secure-openssl.c')
 endif
 
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 9256b426c1d4..c45fe86d1d24 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,5 +1,5 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-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 6ba78adb6786..1846594ec516 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 75ac08f943d9..b1ebab90d4e6 100644
--- a/src/interfaces/libpq/test/Makefile
+++ b/src/interfaces/libpq/test/Makefile
@@ -15,10 +15,17 @@ 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)
 
 $(PROGS): $(WIN32RES)
 
+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/src/interfaces/libpq/test/meson.build b/src/interfaces/libpq/test/meson.build
index 16f94c1ed8b1..491ccf30ff9a 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -13,3 +13,5 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+# TODO: libpq_test_encrypt
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874d3..c8ba1705030b 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -24,7 +24,7 @@ ifeq ($(with_ldap),yes)
 SUBDIRS += ldap
 endif
 ifeq ($(with_ssl),openssl)
-SUBDIRS += ssl
+SUBDIRS += column_encryption ssl
 endif
 
 # Test suites that are not safe by default but can be run if selected
@@ -36,7 +36,7 @@ export PG_TEST_EXTRA
 # 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 000000000000..456dbf69d2a4
--- /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 000000000000..7aca84b17a00
--- /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/meson.build b/src/test/column_encryption/meson.build
new file mode 100644
index 000000000000..a0525da36615
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,20 @@
+column_encryption_test_client = executable('test_client',
+  files('test_client.c'),
+  dependencies: [frontend_code, libpq],
+  kwargs: default_bin_args + {
+    'install': false,
+  },
+)
+testprep_targets += column_encryption_test_client
+
+tests += {
+  'name': 'column_encryption',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_column_encryption.pl',
+      't/002_cmk_rotation.pl',
+    ],
+  },
+}
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 000000000000..4a23414ee0a7
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,183 @@
+# 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{CREATE COLUMN MASTER KEY ${cmkname} WITH (realm = '')});
+	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{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = ${cmkname}, encrypted_value = '\\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{'PGCOLUMNENCRYPTION'} = '1';
+$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),
+    c smallint 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, c) VALUES (1, $1, $2) \gencr 'val1' 11
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \gencr 'val2' 22
+});
+
+# 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}\t\\\\x[0-9a-f]{96}\n2\t\\\\x[0-9a-f]{96}\t\\\\x[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text
+c|smallint),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'decrypted query result');
+
+{
+	local $ENV{'PGCMKLOOKUP'} = '*=run:broken %k "%b"';
+	$result = $node->psql('postgres', q{SELECT a, b, c 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, c FROM tbl1});
+	is($result,
+		q(1|val1|11
+2|val2|22),
+		'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, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22),
+	'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, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3|33),
+	'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, c FROM tbl1});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3upd|33),
+	'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 000000000000..ec447872ab24
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,108 @@
+# 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{CREATE COLUMN MASTER KEY ${cmkname} WITH (realm = '')});
+	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{CREATE COLUMN ENCRYPTION KEY ${cekname} WITH VALUES (column_master_key = cmk1, encrypted_value = '\\x${cekenchex}');});
+
+$ENV{'PGCOLUMNENCRYPTION'} = '1';
+$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{ALTER COLUMN ENCRYPTION KEY ${cekname} ADD VALUE (column_master_key = cmk2, encrypted_value = '\\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{ALTER COLUMN ENCRYPTION KEY ${cekname} DROP VALUE (column_master_key = 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 000000000000..636de88c7851
--- /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", "33"};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					   3, 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", "33"};
+	int			forces[] = {false, true};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b, c) VALUES ($1, $2, $3)",
+					3, 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, "", 3, 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 000000000000..9664349f1401
--- /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/meson.build b/src/test/meson.build
index 241d9d48aa53..0d39cedeb109 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -7,6 +7,7 @@ subdir('subscription')
 subdir('modules')
 
 if ssl.found()
+  subdir('column_encryption')
   subdir('ssl')
 endif
 
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 000000000000..cbd8cd983369
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,143 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE ROLE regress_enc_user1;
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+CREATE COLUMN MASTER KEY cmk2 WITH (
+    realm = 'test2'
+);
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'test2'
+);
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "cek1" already has data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "fail" does not exist
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\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    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+\d tbl_447f
+              Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_447f
+                                        Table "public.tbl_447f"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key cek1 data for master key cmk1 depends on column master key cmk1
+column encryption key cek4 data for master key cmk1 depends on column master key cmk1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ERROR:  column master key "cmk3" already exists
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+ERROR:  column master key "cmkx" does not exist
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ERROR:  column encryption key "cek3" already exists
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET ROLE regress_enc_user1;
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+ERROR:  must be owner of column encryption key cek3
+DROP COLUMN MASTER KEY cmk3;  -- fail
+ERROR:  must be owner of column master key cmk3
+RESET ROLE;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+SET ROLE regress_enc_user1;
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET ROLE;
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ERROR:  column encryption key "cek1" has no data for master key "cmk1a"
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ERROR:  column master key "fail" does not exist
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+ERROR:  attribute "algorithm" must not be specified
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+ERROR:  column encryption key "fail" does not exist
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 3549b63a79c1..3eca6cdb661a 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -37,6 +37,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk WITH (realm = '');
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -105,7 +107,8 @@ BEGIN
 		('text search template'), ('text search configuration'),
 		('policy'), ('user mapping'), ('default acl'), ('transform'),
 		('operator of access method'), ('function of access method'),
-		('publication namespace'), ('publication relation')
+		('publication namespace'), ('publication relation'),
+		('column encryption key'), ('column encryption key data'), ('column master key')
 	LOOP
 		FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
 		LOOP
@@ -325,6 +328,24 @@ WARNING:  error for publication relation,{addr_nsp,zwei},{}: argument list lengt
 WARNING:  error for publication relation,{addr_nsp,zwei},{integer}: relation "addr_nsp.zwei" does not exist
 WARNING:  error for publication relation,{eins,zwei,drei},{}: argument list length must be exactly 1
 WARNING:  error for publication relation,{eins,zwei,drei},{integer}: cross-database references are not implemented: "eins.zwei.drei"
+WARNING:  error for column encryption key,{eins},{}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{}: name list length must be exactly 1
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: name list length must be exactly 1
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: name list length must be exactly 1
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: name list length must be exactly 1
+WARNING:  error for column encryption key data,{eins},{}: argument list length must be exactly 1
+WARNING:  error for column encryption key data,{eins},{integer}: column encryption key "eins" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{}: name list length must be exactly 1
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: name list length must be exactly 1
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: name list length must be exactly 1
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: name list length must be exactly 1
+WARNING:  error for column master key,{eins},{}: column master key "eins" does not exist
+WARNING:  error for column master key,{eins},{integer}: column master key "eins" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{}: name list length must be exactly 1
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: name list length must be exactly 1
+WARNING:  error for column master key,{eins,zwei,drei},{}: name list length must be exactly 1
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: name list length must be exactly 1
 -- these object types cannot be qualified names
 SELECT pg_get_object_address('language', '{one}', '{}');
 ERROR:  language "one" does not exist
@@ -380,6 +401,14 @@ SELECT pg_get_object_address('subscription', '{one}', '{}');
 ERROR:  subscription "one" does not exist
 SELECT pg_get_object_address('subscription', '{one,two}', '{}');
 ERROR:  name list length must be exactly 1
+SELECT pg_get_object_address('column encryption key', '{one}', '{}');
+ERROR:  column encryption key "one" does not exist
+SELECT pg_get_object_address('column encryption key', '{one,two}', '{}');
+ERROR:  name list length must be exactly 1
+SELECT pg_get_object_address('column master key', '{one}', '{}');
+ERROR:  column master key "one" does not exist
+SELECT pg_get_object_address('column master key', '{one,two}', '{}');
+ERROR:  name list length must be exactly 1
 -- test successful cases
 WITH objects (type, name, args) AS (VALUES
 				('table', '{addr_nsp, gentable}'::text[], '{}'::text[]),
@@ -402,6 +431,9 @@ WITH objects (type, name, args) AS (VALUES
 				('type', '{addr_nsp.genenum}', '{}'),
 				('cast', '{int8}', '{int4}'),
 				('collation', '{default}', '{}'),
+				('column encryption key', '{addr_cek}', '{}'),
+				('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+				('column master key', '{addr_cmk}', '{}'),
 				('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
 				('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
 				('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -447,59 +479,62 @@ SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*,
 			pg_identify_object_as_address(classid, objid, objsubid) ioa(typ,nms,args),
 			pg_get_object_address(typ, nms, ioa.args) as addr2
 	ORDER BY addr1.classid, addr1.objid, addr1.objsubid;
-           type            |   schema   |       name        |                               identity                               | ?column? 
----------------------------+------------+-------------------+----------------------------------------------------------------------+----------
- default acl               |            |                   | for role regress_addr_user in schema public on tables                | t
- default acl               |            |                   | for role regress_addr_user on tables                                 | t
- type                      | pg_catalog | _int4             | integer[]                                                            | t
- type                      | addr_nsp   | gencomptype       | addr_nsp.gencomptype                                                 | t
- type                      | addr_nsp   | genenum           | addr_nsp.genenum                                                     | t
- type                      | addr_nsp   | gendomain         | addr_nsp.gendomain                                                   | t
- function                  | pg_catalog |                   | pg_catalog.pg_identify_object(pg_catalog.oid,pg_catalog.oid,integer) | t
- aggregate                 | addr_nsp   |                   | addr_nsp.genaggr(integer)                                            | t
- procedure                 | addr_nsp   |                   | addr_nsp.proc(integer)                                               | t
- sequence                  | addr_nsp   | gentable_a_seq    | addr_nsp.gentable_a_seq                                              | t
- table                     | addr_nsp   | gentable          | addr_nsp.gentable                                                    | t
- table column              | addr_nsp   | gentable          | addr_nsp.gentable.b                                                  | t
- index                     | addr_nsp   | gentable_pkey     | addr_nsp.gentable_pkey                                               | t
- table                     | addr_nsp   | parttable         | addr_nsp.parttable                                                   | t
- index                     | addr_nsp   | parttable_pkey    | addr_nsp.parttable_pkey                                              | t
- view                      | addr_nsp   | genview           | addr_nsp.genview                                                     | t
- materialized view         | addr_nsp   | genmatview        | addr_nsp.genmatview                                                  | t
- foreign table             | addr_nsp   | genftable         | addr_nsp.genftable                                                   | t
- foreign table column      | addr_nsp   | genftable         | addr_nsp.genftable.a                                                 | t
- role                      |            | regress_addr_user | regress_addr_user                                                    | t
- server                    |            | addr_fserv        | addr_fserv                                                           | t
- user mapping              |            |                   | regress_addr_user on server integer                                  | t
- foreign-data wrapper      |            | addr_fdw          | addr_fdw                                                             | t
- access method             |            | btree             | btree                                                                | t
- operator of access method |            |                   | operator 1 (integer, integer) of pg_catalog.integer_ops USING btree  | t
- function of access method |            |                   | function 2 (integer, integer) of pg_catalog.integer_ops USING btree  | t
- default value             |            |                   | for addr_nsp.gentable.b                                              | t
- cast                      |            |                   | (bigint AS integer)                                                  | t
- table constraint          | addr_nsp   |                   | a_chk on addr_nsp.gentable                                           | t
- domain constraint         | addr_nsp   |                   | domconstr on addr_nsp.gendomain                                      | t
- conversion                | pg_catalog | koi8_r_to_mic     | pg_catalog.koi8_r_to_mic                                             | t
- language                  |            | plpgsql           | plpgsql                                                              | t
- schema                    |            | addr_nsp          | addr_nsp                                                             | t
- operator class            | pg_catalog | int4_ops          | pg_catalog.int4_ops USING btree                                      | t
- operator                  | pg_catalog |                   | pg_catalog.+(integer,integer)                                        | t
- rule                      |            |                   | "_RETURN" on addr_nsp.genview                                        | t
- trigger                   |            |                   | t on addr_nsp.gentable                                               | t
- operator family           | pg_catalog | integer_ops       | pg_catalog.integer_ops USING btree                                   | t
- policy                    |            |                   | genpol on addr_nsp.gentable                                          | t
- statistics object         | addr_nsp   | gentable_stat     | addr_nsp.gentable_stat                                               | t
- collation                 | pg_catalog | "default"         | pg_catalog."default"                                                 | t
- transform                 |            |                   | for integer on language sql                                          | t
- text search dictionary    | addr_nsp   | addr_ts_dict      | addr_nsp.addr_ts_dict                                                | t
- text search parser        | addr_nsp   | addr_ts_prs       | addr_nsp.addr_ts_prs                                                 | t
- text search configuration | addr_nsp   | addr_ts_conf      | addr_nsp.addr_ts_conf                                                | t
- text search template      | addr_nsp   | addr_ts_temp      | addr_nsp.addr_ts_temp                                                | t
- subscription              |            | regress_addr_sub  | regress_addr_sub                                                     | t
- publication               |            | addr_pub          | addr_pub                                                             | t
- publication relation      |            |                   | addr_nsp.gentable in publication addr_pub                            | t
- publication namespace     |            |                   | addr_nsp in publication addr_pub_schema                              | t
-(50 rows)
+            type            |   schema   |       name        |                               identity                               | ?column? 
+----------------------------+------------+-------------------+----------------------------------------------------------------------+----------
+ default acl                |            |                   | for role regress_addr_user in schema public on tables                | t
+ default acl                |            |                   | for role regress_addr_user on tables                                 | t
+ type                       | pg_catalog | _int4             | integer[]                                                            | t
+ type                       | addr_nsp   | gencomptype       | addr_nsp.gencomptype                                                 | t
+ type                       | addr_nsp   | genenum           | addr_nsp.genenum                                                     | t
+ type                       | addr_nsp   | gendomain         | addr_nsp.gendomain                                                   | t
+ function                   | pg_catalog |                   | pg_catalog.pg_identify_object(pg_catalog.oid,pg_catalog.oid,integer) | t
+ aggregate                  | addr_nsp   |                   | addr_nsp.genaggr(integer)                                            | t
+ procedure                  | addr_nsp   |                   | addr_nsp.proc(integer)                                               | t
+ sequence                   | addr_nsp   | gentable_a_seq    | addr_nsp.gentable_a_seq                                              | t
+ table                      | addr_nsp   | gentable          | addr_nsp.gentable                                                    | t
+ table column               | addr_nsp   | gentable          | addr_nsp.gentable.b                                                  | t
+ index                      | addr_nsp   | gentable_pkey     | addr_nsp.gentable_pkey                                               | t
+ table                      | addr_nsp   | parttable         | addr_nsp.parttable                                                   | t
+ index                      | addr_nsp   | parttable_pkey    | addr_nsp.parttable_pkey                                              | t
+ view                       | addr_nsp   | genview           | addr_nsp.genview                                                     | t
+ materialized view          | addr_nsp   | genmatview        | addr_nsp.genmatview                                                  | t
+ foreign table              | addr_nsp   | genftable         | addr_nsp.genftable                                                   | t
+ foreign table column       | addr_nsp   | genftable         | addr_nsp.genftable.a                                                 | t
+ role                       |            | regress_addr_user | regress_addr_user                                                    | t
+ server                     |            | addr_fserv        | addr_fserv                                                           | t
+ user mapping               |            |                   | regress_addr_user on server integer                                  | t
+ foreign-data wrapper       |            | addr_fdw          | addr_fdw                                                             | t
+ access method              |            | btree             | btree                                                                | t
+ operator of access method  |            |                   | operator 1 (integer, integer) of pg_catalog.integer_ops USING btree  | t
+ function of access method  |            |                   | function 2 (integer, integer) of pg_catalog.integer_ops USING btree  | t
+ default value              |            |                   | for addr_nsp.gentable.b                                              | t
+ cast                       |            |                   | (bigint AS integer)                                                  | t
+ table constraint           | addr_nsp   |                   | a_chk on addr_nsp.gentable                                           | t
+ domain constraint          | addr_nsp   |                   | domconstr on addr_nsp.gendomain                                      | t
+ conversion                 | pg_catalog | koi8_r_to_mic     | pg_catalog.koi8_r_to_mic                                             | t
+ language                   |            | plpgsql           | plpgsql                                                              | t
+ schema                     |            | addr_nsp          | addr_nsp                                                             | t
+ operator class             | pg_catalog | int4_ops          | pg_catalog.int4_ops USING btree                                      | t
+ operator                   | pg_catalog |                   | pg_catalog.+(integer,integer)                                        | t
+ rule                       |            |                   | "_RETURN" on addr_nsp.genview                                        | t
+ trigger                    |            |                   | t on addr_nsp.gentable                                               | t
+ operator family            | pg_catalog | integer_ops       | pg_catalog.integer_ops USING btree                                   | t
+ policy                     |            |                   | genpol on addr_nsp.gentable                                          | t
+ statistics object          | addr_nsp   | gentable_stat     | addr_nsp.gentable_stat                                               | t
+ collation                  | pg_catalog | "default"         | pg_catalog."default"                                                 | t
+ transform                  |            |                   | for integer on language sql                                          | t
+ text search dictionary     | addr_nsp   | addr_ts_dict      | addr_nsp.addr_ts_dict                                                | t
+ text search parser         | addr_nsp   | addr_ts_prs       | addr_nsp.addr_ts_prs                                                 | t
+ text search configuration  | addr_nsp   | addr_ts_conf      | addr_nsp.addr_ts_conf                                                | t
+ text search template       | addr_nsp   | addr_ts_temp      | addr_nsp.addr_ts_temp                                                | t
+ subscription               |            | regress_addr_sub  | regress_addr_sub                                                     | t
+ publication                |            | addr_pub          | addr_pub                                                             | t
+ publication relation       |            |                   | addr_nsp.gentable in publication addr_pub                            | t
+ publication namespace      |            |                   | addr_nsp in publication addr_pub_schema                              | t
+ column master key          |            | addr_cmk          | addr_cmk                                                             | t
+ column encryption key      |            | addr_cek          | addr_cek                                                             | t
+ column encryption key data |            |                   | of addr_cek for addr_cmk                                             | t
+(53 rows)
 
 ---
 --- Cleanup resources
@@ -513,6 +548,8 @@ drop cascades to user mapping for regress_addr_user on server integer
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 DROP SCHEMA addr_nsp CASCADE;
 NOTICE:  drop cascades to 14 other objects
 DETAIL:  drop cascades to text search dictionary addr_ts_dict
@@ -548,6 +585,9 @@ WITH objects (classid, objid, objsubid) AS (VALUES
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
@@ -578,6 +618,7 @@ WITH objects (classid, objid, objsubid) AS (VALUES
     ('pg_event_trigger'::regclass, 0, 0), -- no event trigger
     ('pg_policy'::regclass, 0, 0), -- no policy
     ('pg_publication'::regclass, 0, 0), -- no publication
+    ('pg_publication_namespace'::regclass, 0, 0), -- no publication namespace
     ('pg_publication_rel'::regclass, 0, 0), -- no publication relation
     ('pg_subscription'::regclass, 0, 0), -- no subscription
     ('pg_transform'::regclass, 0, 0) -- no transformation
@@ -629,5 +670,9 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(subscription,,,)")|("(subscription,,)")|NULL
 ("(publication,,,)")|("(publication,,)")|NULL
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
+("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
+("(""column master key"",,,)")|("(""column master key"",,)")|NULL
+("(""column encryption key"",,,)")|("(""column encryption key"",,)")|NULL
+("(""column encryption key data"",,,)")|("(""column encryption key data"",,)")|NULL
 -- restore normal output mode
 \a\t
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be3e..2aa0e1632317 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 330eb0f7656b..646507f0290c 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 5815e17b39cc..4482a65d2459 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -162,26 +162,26 @@ PREPARE q7(unknown) AS
 -- DML statements
 PREPARE q8 AS
     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;
-SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements
+SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns, result_types FROM pg_prepared_statements
     ORDER BY name;
- name |                            statement                             |                  parameter_types                   |                                                       result_types                                                       
-------+------------------------------------------------------------------+----------------------------------------------------+--------------------------------------------------------------------------------------------------------------------------
- q2   | PREPARE q2(text) AS                                             +| {text}                                             | {name,boolean,boolean}
-      |         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} | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |         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}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    | 
-      |         ORDER BY unique1;                                        |                                                    | 
- q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    | 
- q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {text,path}
-      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    | 
- q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | 
-      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    | 
+ name |                            statement                             |                  parameter_types                   | parameter_orig_tables | parameter_orig_columns |                                                       result_types                                                       
+------+------------------------------------------------------------------+----------------------------------------------------+-----------------------+------------------------+--------------------------------------------------------------------------------------------------------------------------
+ q2   | PREPARE q2(text) AS                                             +| {text}                                             | {-}                   | {0}                    | {name,boolean,boolean}
+      |         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}            | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
+      |         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}                  | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
+      |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    |                       |                        | 
+      |         ORDER BY unique1;                                        |                                                    |                       |                        | 
+ q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {-,-}                 | {0,0}                  | {integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,integer,name,name,name}
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    |                       |                        | 
+ q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {-}                   | {0}                    | {text,path}
+      |     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;
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index a7f5700edc12..2ee5f7546d5b 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 9dd137415e86..7ebf6fdb25a1 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1423,11 +1423,13 @@ pg_prepared_statements| SELECT p.name,
     p.statement,
     p.prepare_time,
     p.parameter_types,
+    p.parameter_orig_tables,
+    p.parameter_orig_columns,
     p.result_types,
     p.from_sql,
     p.generic_plans,
     p.custom_plans
-   FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, result_types, from_sql, generic_plans, custom_plans);
+   FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, parameter_orig_tables, parameter_orig_columns, result_types, from_sql, generic_plans, custom_plans);
 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 d3ac08c9ee3e..357095e1b9b6 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -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,
@@ -171,10 +173,12 @@ WHERE t1.typinput = p1.oid AND t1.typtype in ('b', 'p') AND NOT
     (t1.typelem != 0 AND t1.typlen < 0) AND NOT
     (p1.prorettype = t1.oid AND NOT p1.proretset)
 ORDER BY 1;
- oid  |  typname  | oid | proname 
-------+-----------+-----+---------
- 1790 | refcursor |  46 | textin
-(1 row)
+ oid  |     typname      | oid  | proname 
+------+------------------+------+---------
+ 1790 | refcursor        |   46 | textin
+ 8243 | pg_encrypted_det | 1244 | byteain
+ 8244 | pg_encrypted_rnd | 1244 | byteain
+(3 rows)
 
 -- Varlena array types will point to array_in
 -- Exception as of 8.1: int2vector and oidvector have their own I/O routines
@@ -223,10 +227,12 @@ WHERE t1.typoutput = p1.oid AND t1.typtype in ('b', 'p') AND NOT
       (p1.oid = 'array_out'::regproc AND
        t1.typelem != 0 AND t1.typlen = -1)))
 ORDER BY 1;
- oid  |  typname  | oid | proname 
-------+-----------+-----+---------
- 1790 | refcursor |  47 | textout
-(1 row)
+ oid  |     typname      | oid | proname  
+------+------------------+-----+----------
+ 1790 | refcursor        |  47 | textout
+ 8243 | pg_encrypted_det |  31 | byteaout
+ 8244 | pg_encrypted_rnd |  31 | byteaout
+(3 rows)
 
 SELECT t1.oid, t1.typname, p1.oid, p1.proname
 FROM pg_type AS t1, pg_proc AS p1
@@ -287,10 +293,12 @@ WHERE t1.typreceive = p1.oid AND t1.typtype in ('b', 'p') AND NOT
     (t1.typelem != 0 AND t1.typlen < 0) AND NOT
     (p1.prorettype = t1.oid AND NOT p1.proretset)
 ORDER BY 1;
- oid  |  typname  | oid  | proname  
-------+-----------+------+----------
- 1790 | refcursor | 2414 | textrecv
-(1 row)
+ oid  |     typname      | oid  |  proname  
+------+------------------+------+-----------
+ 1790 | refcursor        | 2414 | textrecv
+ 8243 | pg_encrypted_det | 2412 | bytearecv
+ 8244 | pg_encrypted_rnd | 2412 | bytearecv
+(3 rows)
 
 -- Varlena array types will point to array_recv
 -- Exception as of 8.1: int2vector and oidvector have their own I/O routines
@@ -348,10 +356,12 @@ WHERE t1.typsend = p1.oid AND t1.typtype in ('b', 'p') AND NOT
       (p1.oid = 'array_send'::regproc AND
        t1.typelem != 0 AND t1.typlen = -1)))
 ORDER BY 1;
- oid  |  typname  | oid  | proname  
-------+-----------+------+----------
- 1790 | refcursor | 2415 | textsend
-(1 row)
+ oid  |     typname      | oid  |  proname  
+------+------------------+------+-----------
+ 1790 | refcursor        | 2415 | textsend
+ 8243 | pg_encrypted_det | 2413 | byteasend
+ 8244 | pg_encrypted_rnd | 2413 | byteasend
+(3 rows)
 
 SELECT t1.oid, t1.typname, p1.oid, p1.proname
 FROM pg_type AS t1, pg_proc AS p1
@@ -707,6 +717,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 9f644a0c1b2c..e68bccfdc879 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 a4b354c9e6c0..8ad1f458d503 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 000000000000..d056737ad514
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,124 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE ROLE regress_enc_user1;
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk2 WITH (
+    realm = 'test2'
+);
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'test2'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+COMMENT ON COLUMN ENCRYPTION KEY cek1 IS 'column encryption key';
+
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+-- duplicate
+ALTER COLUMN ENCRYPTION KEY cek1 ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+ALTER COLUMN ENCRYPTION KEY fail ADD VALUE (
+    column_master_key = cmk1a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek2 WITH VALUES (
+    column_master_key = cmk2,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+),
+(
+    column_master_key = cmk2a,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek4 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\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
+
+CREATE TABLE tbl_447f (
+    a int,
+    b text
+);
+
+ALTER TABLE tbl_447f ADD COLUMN c text ENCRYPTED WITH (column_encryption_key = cek1);
+
+\d tbl_447f
+\d+ tbl_447f
+
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+
+ALTER COLUMN MASTER KEY cmk2 RENAME TO cmk3;
+ALTER COLUMN MASTER KEY cmk1 RENAME TO cmk3;  -- fail
+ALTER COLUMN MASTER KEY cmkx RENAME TO cmky;  -- fail
+
+ALTER COLUMN ENCRYPTION KEY cek2 RENAME TO cek3;
+ALTER COLUMN ENCRYPTION KEY cek1 RENAME TO cek3;  -- fail
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+
+SET ROLE regress_enc_user1;
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET ROLE;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+SET ROLE regress_enc_user1;
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET ROLE;
+
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = fail);  -- fail
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, algorithm = 'foo');  -- fail
+
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index e91072a75d1d..c9f5f0ced9f2 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -40,6 +40,8 @@ CREATE SERVER "integer" FOREIGN DATA WRAPPER addr_fdw;
 CREATE USER MAPPING FOR regress_addr_user SERVER "integer";
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user IN SCHEMA public GRANT ALL ON TABLES TO regress_addr_user;
 ALTER DEFAULT PRIVILEGES FOR ROLE regress_addr_user REVOKE DELETE ON TABLES FROM regress_addr_user;
+CREATE COLUMN MASTER KEY addr_cmk WITH (realm = '');
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, encrypted_value = '');
 -- this transform would be quite unsafe to leave lying around,
 -- except that the SQL language pays no attention to transforms:
 CREATE TRANSFORM FOR int LANGUAGE SQL (
@@ -98,7 +100,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 		('text search template'), ('text search configuration'),
 		('policy'), ('user mapping'), ('default acl'), ('transform'),
 		('operator of access method'), ('function of access method'),
-		('publication namespace'), ('publication relation')
+		('publication namespace'), ('publication relation'),
+		('column encryption key'), ('column encryption key data'), ('column master key')
 	LOOP
 		FOR names IN VALUES ('{eins}'), ('{addr_nsp, zwei}'), ('{eins, zwei, drei}')
 		LOOP
@@ -143,6 +146,10 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 SELECT pg_get_object_address('publication', '{one,two}', '{}');
 SELECT pg_get_object_address('subscription', '{one}', '{}');
 SELECT pg_get_object_address('subscription', '{one,two}', '{}');
+SELECT pg_get_object_address('column encryption key', '{one}', '{}');
+SELECT pg_get_object_address('column encryption key', '{one,two}', '{}');
+SELECT pg_get_object_address('column master key', '{one}', '{}');
+SELECT pg_get_object_address('column master key', '{one,two}', '{}');
 
 -- test successful cases
 WITH objects (type, name, args) AS (VALUES
@@ -166,6 +173,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 				('type', '{addr_nsp.genenum}', '{}'),
 				('cast', '{int8}', '{int4}'),
 				('collation', '{default}', '{}'),
+				('column encryption key', '{addr_cek}', '{}'),
+				('column encryption key data', '{addr_cek}', '{addr_cmk}'),
+				('column master key', '{addr_cmk}', '{}'),
 				('table constraint', '{addr_nsp, gentable, a_chk}', '{}'),
 				('domain constraint', '{addr_nsp.gendomain}', '{domconstr}'),
 				('conversion', '{pg_catalog, koi8_r_to_mic}', '{}'),
@@ -219,6 +229,8 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
 DROP PUBLICATION addr_pub;
 DROP PUBLICATION addr_pub_schema;
 DROP SUBSCRIPTION regress_addr_sub;
+DROP COLUMN ENCRYPTION KEY addr_cek;
+DROP COLUMN MASTER KEY addr_cmk;
 
 DROP SCHEMA addr_nsp CASCADE;
 
@@ -243,6 +255,9 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('pg_type'::regclass, 0, 0), -- no type
     ('pg_cast'::regclass, 0, 0), -- no cast
     ('pg_collation'::regclass, 0, 0), -- no collation
+    ('pg_colenckey'::regclass, 0, 0), -- no column encryption key
+    ('pg_colenckeydata'::regclass, 0, 0), -- no column encryption key data
+    ('pg_colmasterkey'::regclass, 0, 0), -- no column master key
     ('pg_constraint'::regclass, 0, 0), -- no constraint
     ('pg_conversion'::regclass, 0, 0), -- no conversion
     ('pg_attrdef'::regclass, 0, 0), -- no default attribute
@@ -273,6 +288,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
     ('pg_event_trigger'::regclass, 0, 0), -- no event trigger
     ('pg_policy'::regclass, 0, 0), -- no policy
     ('pg_publication'::regclass, 0, 0), -- no publication
+    ('pg_publication_namespace'::regclass, 0, 0), -- no publication namespace
     ('pg_publication_rel'::regclass, 0, 0), -- no publication relation
     ('pg_subscription'::regclass, 0, 0), -- no subscription
     ('pg_transform'::regclass, 0, 0) -- no transformation
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95cee..7db788735c7f 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -75,7 +75,7 @@ CREATE TEMPORARY TABLE q5_prep_nodata AS EXECUTE q5(200, 'DTAAAA')
 PREPARE q8 AS
     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;
 
-SELECT name, statement, parameter_types, result_types FROM pg_prepared_statements
+SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns, result_types 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 1149c6a839ef..e88c70d3024c 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 5edc1f1f6ed0..0ffe45fd3707 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -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,

base-commit: 4148c8b3daf95ca308f055e103f6ee82e25b8f99
-- 
2.37.3



view thread (53+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected]
  Subject: Re: Transparent column encryption
  In-Reply-To: <[email protected]>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox