Transparent column encryption

Started by Peter Eisentrautabout 4 years ago103 messages
#1Peter Eisentraut
peter.eisentraut@enterprisedb.com
1 attachment(s)

I want to present my proof-of-concept patch for the transparent column
encryption feature. (Some might also think of it as automatic
client-side encryption or similar, but I like my name.) 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
the "prying eyes" of DBAs, sysadmins, cloud operators, 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. Of course, you 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 you can do equality lookups, at the cost of some
security.

This functionality also exists in other SQL database products, so the
overall concepts weren't invented by me by any means.

Also, this feature has nothing to do with the on-disk encryption
feature being contemplated in parallel. Both can exist independently.

The attached patch 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. But it's missing the remaining 90% of the work,
including additional DDL support, error handling, robust memory
management, protocol versioning, forward and backward compatibility,
pg_dump support, psql \d support, refinement of the cryptography, and
so on. But I think obvious solutions exist to all of those things, so
it isn't that interesting to focus on them for now.

------

Now to the explanation of how it works.

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, the catalog object specifies a "provider" and generic
options. Right now, libpq has a "file" provider hardcoded, and it
takes a "filename" option. Via some mechanism to be determined,
additional providers could be loaded and then talk to key management
systems via http or whatever. I have left some comments in the libpq
code where the hook points for this could be.

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.)

The encryption algorithms are mostly hardcoded right now, but there
are facilities for picking algorithms and adding new ones that will be
expanded. The CMK process uses RSA-OAEP. The CEK process uses
AES-128-CBC right now; a more complete solution should probably
involve some HMAC thrown in.

In the server, the encrypted datums are stored in types called
encryptedr and encryptedd (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, encryptedr has no operators at all,
encryptedd 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.

Some protocol extensions are required. These should be guarded by
some _pq_... setting, but this is not done in this patch yet. As
mentioned above, extra messages are added for sending the CMKs and
CEKs. In the RowDescription message, I have commandeered the format
field to add a bit that indicates that the field is encrypted. This
could be made a separate field, and there should probably be
additional fields to indicate the algorithm and CEK name, but this was
easiest for now. The ParameterDescription message is extended to
contain format fields for each parameter, for the same purpose.
Again, this could be done differently.

Speaking of parameter descriptions, 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.

And also, psql doesn't use prepared statements, so writing into
encrypted columns currently doesn't work at all via psql. (Reading
works no problem.) All the test code currently uses custom libpq C
programs. We should think about a way to enable prepared statements
in psql, perhaps something like

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

(\gexec and \gx are already taken.)

------

This is not targeting PostgreSQL 15. But I'd appreciate some feedback
on the direction. As I mentioned above, a lot of the remaining work
is arguably mostly straightforward. Some closer examination of the
issues surrounding the libpq API changes and psql would be useful.
Perhaps there are other projects where that kind of functionality
would also be useful.

Attachments:

v1-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v1-0001-Transparent-column-encryption.patchDownload
From 84281a522bedc1b83bf4fbea5390b8b1f1591152 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Fri, 3 Dec 2021 22:07:52 +0100
Subject: [PATCH v1] Transparent column encryption

---
 doc/src/sgml/catalogs.sgml                    | 227 +++++++++++
 doc/src/sgml/datatype.sgml                    |  47 +++
 doc/src/sgml/ddl.sgml                         |  65 ++++
 doc/src/sgml/libpq.sgml                       |  79 ++++
 doc/src/sgml/protocol.sgml                    | 220 ++++++++++-
 doc/src/sgml/ref/create_table.sgml            |  42 ++-
 src/backend/access/common/printtup.c          | 150 ++++++++
 src/backend/access/common/tupdesc.c           |   8 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/heap.c                    |   3 +
 src/backend/commands/prepare.c                |  51 ++-
 src/backend/commands/tablecmds.c              |  62 +++
 src/backend/executor/spi.c                    |   4 +
 src/backend/nodes/copyfuncs.c                 |   1 +
 src/backend/nodes/equalfuncs.c                |   1 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/nodes/outfuncs.c                  |   1 +
 src/backend/parser/analyze.c                  |   5 +-
 src/backend/parser/gram.y                     |  13 +-
 src/backend/parser/parse_param.c              |  43 ++-
 src/backend/parser/parse_target.c             |   6 +
 src/backend/tcop/postgres.c                   |  58 ++-
 src/backend/utils/cache/plancache.c           |  10 +
 src/backend/utils/cache/syscache.c            |  29 ++
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |   9 +
 src/include/catalog/pg_cast.dat               |   6 +
 src/include/catalog/pg_colenckey.h            |  18 +
 src/include/catalog/pg_colenckeydata.h        |  25 ++
 src/include/catalog/pg_colmasterkey.h         |  24 ++
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  10 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               |  13 +-
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/analyze.h                  |   3 +-
 src/include/parser/parse_node.h               |   2 +
 src/include/parser/parse_param.h              |   3 +-
 src/include/utils/plancache.h                 |   4 +
 src/include/utils/syscache.h                  |   3 +
 src/interfaces/libpq/exports.txt              |   2 +
 src/interfaces/libpq/fe-connect.c             |   5 +
 src/interfaces/libpq/fe-exec.c                | 352 +++++++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           |  83 ++++-
 src/interfaces/libpq/fe-trace.c               |   2 +
 src/interfaces/libpq/libpq-fe.h               |  16 +
 src/interfaces/libpq/libpq-int.h              |  13 +
 src/test/Makefile                             |   3 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  20 +
 .../t/001_column_encryption.pl                | 135 +++++++
 src/test/column_encryption/test_client.c      | 155 ++++++++
 .../libpq_pipeline/traces/prepared.trace      |   2 +-
 .../regress/expected/column_encryption.out    |  47 +++
 src/test/regress/expected/oidjoins.out        |   4 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/prepare.out         |  41 +-
 src/test/regress/expected/rules.out           |   6 +-
 src/test/regress/expected/sanity_check.out    |   3 +
 src/test/regress/expected/type_sanity.out     |  20 +-
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/sql/column_encryption.sql    |  44 +++
 src/test/regress/sql/prepare.sql              |   6 +-
 src/test/regress/sql/type_sanity.sql          |   4 +-
 69 files changed, 2196 insertions(+), 67 deletions(-)
 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/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/test_client.c
 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/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index c1d11be73f..a220aa4dd2 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>
@@ -2423,6 +2438,218 @@ <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>
+    </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>text</type>
+      </para>
+      <para>
+       The encryption algorithm used for encrypting the key material
+      </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>cmkprovider</structfield> <type>text</type>
+      </para>
+      <para>
+       The provider associated with this column master key.  This is used by
+       clients to determine how to look up the key.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkoptions</structfield> <type>text[]</type>
+      </para>
+      <para>
+       Provider-specific options, such as a file name or a URL.
+      </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 6929f3bb18..bcda249179 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5341,4 +5341,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>encryptedr</type> (for randomized encryption) or
+    <type>encryptedd</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>encryptedd</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>encryptedd</type></entry>
+        <entry>1 or 4 bytes plus the actual binary string</entry>
+        <entry>encrypted column value, deterministic encryption</entry>
+       </row>
+       <row>
+        <entry><type>encryptedr</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 642ea2a70d..b0c26958a7 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1161,6 +1161,71 @@ <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>
+
+  <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> 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>
+ </sect1>
+
  <sect1 id="ddl-system-columns">
   <title>System Columns</title>
 
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index c17d33a54f..f1bb5e9df5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -3012,6 +3012,44 @@ <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,
+                         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>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -4563,6 +4601,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
@@ -4570,6 +4609,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.
@@ -4681,6 +4721,44 @@ <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 PQsendQueryPrepared(PGconn *conn,
+                        const char *stmtName,
+                        int nParams,
+                        const char * const *paramValues,
+                        const int *paramLengths,
+                        const int *paramFormats,
+                        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.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4731,6 +4809,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"/>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 34a7034282..9cb8aad8d6 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1050,6 +1050,47 @@ <title>Extended Query</title>
    </note>
   </sect2>
 
+  <sect2>
+   <title>Transparent Column Encryption</title>
+
+   <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 a name
+    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>
+  </sect2>
+
   <sect2>
    <title>Function Call</title>
 
@@ -4083,6 +4124,159 @@ <title>Message Formats</title>
 </varlistentry>
 
 
+<varlistentry>
+<term>
+ColumnEncryptionKey (B)
+</term>
+<listitem>
+<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>
+        String
+</term>
+<listitem>
+<para>
+                The name of the 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.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+
+</para>
+</listitem>
+</varlistentry>
+
+
+<varlistentry>
+<term>
+ColumnMasterKey (B)
+</term>
+<listitem>
+<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>
+        String
+</term>
+<listitem>
+<para>
+                The name of the key.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+        String
+</term>
+<listitem>
+<para>
+                The name of the key provider.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+        Int16
+</term>
+<listitem>
+<para>
+                The number of options to follow.
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+        Next, the following fields follow for each option:
+<variablelist>
+<varlistentry>
+<term>
+        String
+</term>
+<listitem>
+<para>
+                Option name
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+        String
+</term>
+<listitem>
+<para>
+                Option value
+</para>
+</listitem>
+</varlistentry>
+</variablelist>
+
+</para>
+</listitem>
+</varlistentry>
+
+
 <varlistentry>
 <term>
 CommandComplete (B)
@@ -5408,6 +5602,23 @@ <title>Message Formats</title>
 </para>
 </listitem>
 </varlistentry>
+</variablelist>
+        And then, for each parameter, there is the following:
+<variablelist>
+<varlistentry>
+<term>
+        Int16
+</term>
+<listitem>
+<para>
+                A format code.  This is used as a bit field.  If bit 0x10 is
+                set, the parameter corresponds to an encrypted column and must
+                be sent encrypted.  If bit 0x20 is set in addition, the column
+                uses deterministic encryption, otherwise randomized
+                encryption.
+</para>
+</listitem>
+</varlistentry>
 </variablelist>
 </para>
 </listitem>
@@ -5876,10 +6087,11 @@ <title>Message Formats</title>
 </term>
 <listitem>
 <para>
-                The format code being used for the field.  Currently will
-                be zero (text) or one (binary).  In a RowDescription
-                returned from the statement variant of Describe, the
-                format code is not yet known and will always be zero.
+                The format code being used for the field.  The lower byte will
+                be zero (text) or one (binary).  The upper byte will be 1 if
+                the column is encrypted.  In a RowDescription returned from
+                the statement variant of Describe, the format code is not yet
+                known, so the lower byte will always be zero.
 </para>
 </listitem>
 </varlistentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 57d51a676a..d72043d2b7 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> [ 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> [ COMPRESSION <replaceable>compression_method</replaceable> ] [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -317,6 +317,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>AES-CBC-128</literal>.
+         </para>
+        </listitem>
+       </varlistentry>
+      </variablelist>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 54b539f6fb..19a27325d8 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,25 @@
  */
 #include "postgres.h"
 
+#include "access/genam.h"
 #include "access/printtup.h"
+#include "access/skey.h"
+#include "access/table.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/memdebug.h"
 #include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
 
 
 static void printtup_startup(DestReceiver *self, int operation,
@@ -151,6 +163,127 @@ printtup_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 	 */
 }
 
+List *cmk_sent = NIL;
+
+static void
+MaybeSendColumnMasterKeyMessage(Oid cmkid)
+{
+	HeapTuple	tuple;
+	Form_pg_colmasterkey cmkform;
+	Datum		datum;
+	bool		isnull;
+	int			n;
+	AnyArrayType *arr;
+	array_iter	iter;
+	StringInfoData buf;
+	MemoryContext oldcontext;
+
+	if (list_member_oid(cmk_sent, cmkid))
+		return;
+
+	tuple = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	cmkform = (Form_pg_colmasterkey) GETSTRUCT(tuple);
+
+	pq_beginmessage(&buf, 'y'); /* ColumnMasterKey */
+	pq_sendstring(&buf, NameStr(cmkform->cmkname));
+
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkprovider, &isnull);
+	Assert(!isnull);
+	pq_sendstring(&buf, TextDatumGetCString(datum));
+
+	datum = SysCacheGetAttr(CMKOID, tuple, Anum_pg_colmasterkey_cmkoptions, &isnull);
+	Assert(!isnull);
+	arr = DatumGetAnyArrayP(datum);
+	Assert(ARR_NDIM((ArrayType *) arr) == 1);
+	n = ARR_DIMS((ArrayType *) arr)[0];
+	Assert(n % 2 == 0);
+	pq_sendint16(&buf, n / 2);
+
+	array_iter_setup(&iter, arr);
+
+	for (int i = 0; i < n; i++)
+	{
+		datum = array_iter_next(&iter, &isnull, i, -1, false, 'i');
+		Assert(!isnull);
+		pq_sendstring(&buf, TextDatumGetCString(datum));
+	}
+
+	pq_endmessage(&buf);
+
+	ReleaseSysCache(tuple);
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cmk_sent = lappend_oid(cmk_sent, cmkid);
+	MemoryContextSwitchTo(oldcontext);
+}
+
+List *cek_sent = NIL;
+
+void
+MaybeSendColumnEncryptionKeyMessage(Oid attcek)
+{
+	HeapTuple	tuple;
+	HeapTuple	tuple2;
+	Form_pg_colenckey cekform;
+	ScanKeyData	skey[1];
+	SysScanDesc	sd;
+	Relation	rel;
+	bool		found = false;
+	MemoryContext oldcontext;
+
+	if (list_member_oid(cek_sent, attcek))
+		return;
+
+	tuple = SearchSysCache1(CEKOID, ObjectIdGetDatum(attcek));
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+	cekform = (Form_pg_colenckey) GETSTRUCT(tuple);
+
+	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 ((tuple2 = systable_getnext(sd)))
+	{
+		Form_pg_colenckeydata ckdform = (Form_pg_colenckeydata) GETSTRUCT(tuple2);
+		Datum		datum;
+		bool		isnull;
+		bytea	   *ba;
+		StringInfoData buf;
+
+		MaybeSendColumnMasterKeyMessage(ckdform->ckdcmkid);
+
+		datum = heap_getattr(tuple2, Anum_pg_colenckeydata_ckdencval, RelationGetDescr(rel), &isnull);
+		Assert(!isnull);
+		ba = pg_detoast_datum_packed((bytea *) DatumGetPointer(datum));
+
+		pq_beginmessage(&buf, 'Y'); /* ColumnEncryptionKey */
+		pq_sendstring(&buf, NameStr(cekform->cekname));
+		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);
+
+	ReleaseSysCache(tuple);
+
+	oldcontext = MemoryContextSwitchTo(TopMemoryContext);
+	cek_sent = lappend_oid(cek_sent, attcek);
+	MemoryContextSwitchTo(oldcontext);
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -231,6 +364,23 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (get_typtype(atttypid) == TYPTYPE_ENCRYPTED)
+		{
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(resorigtbl), Int16GetDatum(resorigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", resorigcol, resorigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			atttypid = orig_att->attrealtypid;
+
+			ReleaseSysCache(tp);
+
+			format |= 0x10;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 9b506471fe..602e9e10a7 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,10 @@ 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->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +633,8 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +696,8 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 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 1e343df0af..3c3dee5c36 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 == ENCRYPTEDDOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 4e6efda97f..85d6ee76e6 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -69,7 +69,8 @@ CATALOG_HEADERS := \
 	pg_default_acl.h pg_init_privs.h pg_seclabel.h pg_shseclabel.h \
 	pg_collation.h pg_partitioned_table.h pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 8bbf23e452..3b8e0b92a1 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -792,6 +792,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] = Int32GetDatum(attrs->attencalg);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 5e03c7c5aa..7925007c41 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -61,6 +61,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	RawStmt    *rawstmt;
 	CachedPlanSource *plansource;
 	Oid		   *argtypes = NULL;
+	Oid		   *argorigtbls = NULL;
+	AttrNumber *argorigcols = NULL;
 	int			nargs;
 	Query	   *query;
 	List	   *query_list;
@@ -108,6 +110,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 
 			argtypes[i++] = toid;
 		}
+
+		argorigtbls = (Oid *) palloc0(nargs * sizeof(Oid));
+		argorigcols = (AttrNumber *) palloc0(nargs * sizeof(AttrNumber));
 	}
 
 	/*
@@ -116,7 +121,7 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * information about unknown parameters to be deduced from context.
 	 */
 	query = parse_analyze_varparams(rawstmt, pstate->p_sourcetext,
-									&argtypes, &nargs);
+									&argtypes, &nargs, &argorigtbls, &argorigcols);
 
 	/*
 	 * Check that all parameter types were determined.
@@ -159,6 +164,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 					   NULL,
 					   argtypes,
 					   nargs,
+					   argorigtbls,
+					   argorigcols,
 					   NULL,
 					   NULL,
 					   CURSOR_OPT_PARALLEL_OK,	/* allow parallel mode */
@@ -724,7 +731,7 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 	 * build tupdesc for result tuples. This must match the definition of the
 	 * pg_prepared_statements view in system_views.sql
 	 */
-	tupdesc = CreateTemplateTupleDesc(7);
+	tupdesc = CreateTemplateTupleDesc(9);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "name",
 					   TEXTOID, -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "statement",
@@ -739,6 +746,10 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 					   INT8OID, -1, 0);
 	TupleDescInitEntry(tupdesc, (AttrNumber) 7, "custom_plans",
 					   INT8OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 8, "parameter_orig_tables",
+					   REGCLASSARRAYOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 9, "parameter_orig_columns",
+					   INT2ARRAYOID, -1, 0);
 
 	/*
 	 * We put all the tuples into a tuplestore in one scan of the hashtable.
@@ -760,8 +771,8 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 		hash_seq_init(&hash_seq, prepared_queries);
 		while ((prep_stmt = hash_seq_search(&hash_seq)) != NULL)
 		{
-			Datum		values[7];
-			bool		nulls[7];
+			Datum		values[9];
+			bool		nulls[9];
 
 			MemSet(nulls, 0, sizeof(nulls));
 
@@ -773,6 +784,38 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 			values[4] = BoolGetDatum(prep_stmt->from_sql);
 			values[5] = Int64GetDatumFast(prep_stmt->plansource->num_generic_plans);
 			values[6] = Int64GetDatumFast(prep_stmt->plansource->num_custom_plans);
+			{
+				int			num_params = prep_stmt->plansource->num_params;
+				Datum      *tmp_ary;
+				ArrayType  *result;
+				int         i;
+
+				tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+
+				for (i = 0; i < num_params; i++)
+					tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_origtbls[i]);
+
+				/* XXX: this hardcodes assumptions about the regclass type */
+				result = construct_array(tmp_ary, num_params, REGCLASSOID,
+										 4, true, TYPALIGN_INT);
+				values[7] = PointerGetDatum(result);
+			}
+			{
+				int			num_params = prep_stmt->plansource->num_params;
+				Datum      *tmp_ary;
+				ArrayType  *result;
+				int         i;
+
+				tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+
+				for (i = 0; i < num_params; i++)
+					tmp_ary[i] = Int16GetDatum(prep_stmt->plansource->param_origcols[i]);
+
+				/* XXX: this hardcodes assumptions about the int2 type */
+				result = construct_array(tmp_ary, num_params, INT2OID,
+										 2, true, TYPALIGN_SHORT);
+				values[8] = PointerGetDatum(result);
+			}
 
 			tuplestore_putvalues(tupstore, tupdesc, values, nulls);
 		}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c35f09998c..8dea6fafd3 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -33,6 +33,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
@@ -900,6 +901,64 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 		if (colDef->compression)
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
+
+		if (colDef->encryption)
+		{
+			ListCell *lc;
+			char *cek = NULL;
+			Oid cekoid;
+			bool encdet = false;
+			int alg = 1;
+
+			foreach(lc, colDef->encryption)
+			{
+				DefElem *el = lfirst_node(DefElem, lc);
+
+				if (strcmp(el->defname, "column_encryption_key") == 0)
+					cek = strVal(linitial(castNode(TypeName, el->arg)->names));
+				else if (strcmp(el->defname, "encryption_type") == 0)
+				{
+					char *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+					if (strcmp(val, "deterministic") == 0)
+						encdet = true;
+					else if (strcmp(val, "randomized") == 0)
+						encdet = false;
+					else
+						elog(ERROR, "unrecognized encryption type: %s", val);
+				}
+				else if (strcmp(el->defname, "algorithm") == 0)
+				{
+					char *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+					if (strcmp(val, "AES-CBC-128") == 0)
+						alg = 1;
+					if (strcmp(val, "AES-CBC-192") == 0)
+						alg = 2;
+					if (strcmp(val, "AES-CBC-256") == 0)
+						alg = 3;
+					else
+						elog(ERROR, "unrecognized encryption algorithm: %s", val);
+				}
+				else
+					elog(ERROR, "unrecognized column encryption parameter: %s", el->defname);
+			}
+
+			if (!cek)
+				elog(ERROR, "column encryption key must be specified");
+
+			cekoid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid, PointerGetDatum(cek));
+			if (!cekoid)
+				elog(ERROR, "column encryption key \"%s\" does not exist", cek);
+
+			attr->attcek = cekoid;
+			attr->attrealtypid = attr->atttypid;
+			if (encdet)
+				attr->atttypid = ENCRYPTEDDOID;
+			else
+				attr->atttypid = ENCRYPTEDROID;
+			attr->attencalg = alg;
+		}
 	}
 
 	/*
@@ -6758,6 +6817,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attgenerated = colDef->generated;
 	attribute.attisdropped = false;
 	attribute.attislocal = colDef->is_local;
+	attribute.attcek = 0; // TODO
+	attribute.attrealtypid = 0; // TODO
+	attribute.attencalg = 0; // TODO
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
 
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 0568ae123f..b1bfe12eab 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2186,6 +2186,8 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan)
 						   NULL,
 						   plan->argtypes,
 						   plan->nargs,
+						   NULL,
+						   NULL,
 						   plan->parserSetup,
 						   plan->parserSetupArg,
 						   plan->cursor_options,
@@ -2423,6 +2425,8 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 							   NULL,
 							   plan->argtypes,
 							   plan->nargs,
+							   NULL,
+							   NULL,
 							   plan->parserSetup,
 							   plan->parserSetupArg,
 							   plan->cursor_options,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 297b6ee715..4f6255baa9 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3035,6 +3035,7 @@ _copyColumnDef(const ColumnDef *from)
 	COPY_STRING_FIELD(colname);
 	COPY_NODE_FIELD(typeName);
 	COPY_STRING_FIELD(compression);
+	COPY_NODE_FIELD(encryption);
 	COPY_SCALAR_FIELD(inhcount);
 	COPY_SCALAR_FIELD(is_local);
 	COPY_SCALAR_FIELD(is_not_null);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index f537d3eb96..cf0a23a2c4 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2679,6 +2679,7 @@ _equalColumnDef(const ColumnDef *a, const ColumnDef *b)
 	COMPARE_STRING_FIELD(colname);
 	COMPARE_NODE_FIELD(typeName);
 	COMPARE_STRING_FIELD(compression);
+	COMPARE_NODE_FIELD(encryption);
 	COMPARE_SCALAR_FIELD(inhcount);
 	COMPARE_SCALAR_FIELD(is_local);
 	COMPARE_SCALAR_FIELD(is_not_null);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index e276264882..33621141f1 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3917,6 +3917,8 @@ raw_expression_tree_walker(Node *node,
 					return true;
 				if (walker(coldef->compression, context))
 					return true;
+				if (walker(coldef->encryption, context))
+					return true;
 				if (walker(coldef->raw_default, context))
 					return true;
 				if (walker(coldef->collClause, context))
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 3600c9fa36..ecd2e86abf 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2953,6 +2953,7 @@ _outColumnDef(StringInfo str, const ColumnDef *node)
 	WRITE_STRING_FIELD(colname);
 	WRITE_NODE_FIELD(typeName);
 	WRITE_STRING_FIELD(compression);
+	WRITE_NODE_FIELD(encryption);
 	WRITE_INT_FIELD(inhcount);
 	WRITE_BOOL_FIELD(is_local);
 	WRITE_BOOL_FIELD(is_not_null);
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 146ee8dd1e..411c474a89 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -148,7 +148,8 @@ parse_analyze(RawStmt *parseTree, const char *sourceText,
  */
 Query *
 parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
-						Oid **paramTypes, int *numParams)
+						Oid **paramTypes, int *numParams,
+						Oid **paramOrigTbls, AttrNumber **paramOrigCols)
 {
 	ParseState *pstate = make_parsestate(NULL);
 	Query	   *query;
@@ -158,7 +159,7 @@ parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
 
 	pstate->p_sourcetext = sourceText;
 
-	parse_variable_parameters(pstate, paramTypes, numParams);
+	parse_variable_parameters(pstate, paramTypes, numParams, paramOrigTbls, paramOrigCols);
 
 	query = transformTopLevelStmt(pstate, parseTree);
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 86ce33bd97..449dc4e9b1 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -568,6 +568,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
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_actions key_delete key_match key_update key_action
@@ -3476,12 +3477,13 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_compression opt_column_encryption create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 					n->colname = $1;
 					n->typeName = $2;
 					n->compression = $3;
+					n->encryption = $4;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3490,8 +3492,8 @@ columnDef:	ColId Typename opt_column_compression create_generic_options ColQualL
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $4;
-					SplitColQualList($5, &n->constraints, &n->collClause,
+					n->fdwoptions = $5;
+					SplitColQualList($6, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *)n;
@@ -3546,6 +3548,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 ColQualList:
 			ColQualList ColConstraint				{ $$ = lappend($1, $2); }
 			| /*EMPTY*/								{ $$ = NIL; }
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index 68a5534393..4fd5cb1443 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;
+	AttrNumber **paramOrigCols;
 } 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 @@ parse_fixed_parameters(ParseState *pstate,
  */
 void
 parse_variable_parameters(ParseState *pstate,
-						  Oid **paramTypes, int *numParams)
+						  Oid **paramTypes, int *numParams,
+						  Oid **paramOrigTbls, AttrNumber **paramOrigCols)
 {
 	VarParamState *parstate = palloc(sizeof(VarParamState));
 
 	parstate->paramTypes = paramTypes;
 	parstate->numParams = numParams;
+	parstate->paramOrigTbls = paramOrigTbls;
+	parstate->paramOrigCols = paramOrigCols;
 	pstate->p_ref_hook_state = (void *) parstate;
 	pstate->p_paramref_hook = variable_paramref_hook;
 	pstate->p_coerce_param_hook = variable_coerce_param_hook;
+	pstate->p_param_assign_orig_hook = variable_param_assign_orig_hook;
 }
 
 /*
@@ -145,14 +152,36 @@ variable_paramref_hook(ParseState *pstate, ParamRef *pref)
 	{
 		/* Need to enlarge param array */
 		if (*parstate->paramTypes)
+		{
 			*parstate->paramTypes = (Oid *) repalloc(*parstate->paramTypes,
 													 paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc(*parstate->paramOrigTbls,
+													paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc(*parstate->paramOrigCols,
+													paramno * sizeof(AttrNumber));
+		}
 		else
+		{
 			*parstate->paramTypes = (Oid *) palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc(paramno * sizeof(AttrNumber));
+		}
 		/* Zero out the previously-unreferenced slots */
 		MemSet(*parstate->paramTypes + *parstate->numParams,
 			   0,
 			   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigTbls)
+			MemSet(*parstate->paramOrigTbls + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigCols)
+			MemSet(*parstate->paramOrigCols + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(AttrNumber));
 		*parstate->numParams = paramno;
 	}
 
@@ -260,6 +289,18 @@ variable_coerce_param_hook(ParseState *pstate, Param *param,
 	return NULL;
 }
 
+static void
+variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol)
+{
+	VarParamState *parstate = (VarParamState *) pstate->p_ref_hook_state;
+	int			paramno = param->paramid;
+
+	if (parstate->paramOrigTbls)
+		(*parstate->paramOrigTbls)[paramno - 1] = origtbl;
+	if (parstate->paramOrigCols)
+		(*parstate->paramOrigCols)[paramno - 1] = origcol;
+}
+
 /*
  * Check for consistent assignment of variable parameters after completion
  * of parsing with parse_variable_parameters.
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 9ce3a0de96..5d0093d7ac 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -595,6 +595,12 @@ transformAssignedExpr(ParseState *pstate,
 					 parser_errposition(pstate, exprLocation(orig_expr))));
 	}
 
+	if (IsA(expr, Param))
+	{
+		if (pstate->p_param_assign_orig_hook)
+			pstate->p_param_assign_orig_hook(pstate, castNode(Param, expr), RelationGetRelid(pstate->p_target_relation), attrno);
+	}
+
 	pstate->p_expr_kind = sv_expr_kind;
 
 	return expr;
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 82de01cdc6..da7ab9d68b 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -80,6 +80,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"
 
@@ -1336,6 +1337,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	bool		is_named;
 	bool		save_log_statement_stats = log_statement_stats;
 	char		msec_str[32];
+	Oid		   *paramOrigTbls = palloc(numParams * sizeof(Oid));
+	AttrNumber *paramOrigCols = palloc(numParams * sizeof(AttrNumber));
 
 	/*
 	 * Report query to various monitoring facilities.
@@ -1459,7 +1462,9 @@ exec_parse_message(const char *query_string,	/* string to execute */
 		query = parse_analyze_varparams(raw_parse_tree,
 										query_string,
 										&paramTypes,
-										&numParams);
+										&numParams,
+										&paramOrigTbls,
+										&paramOrigCols);
 
 		/*
 		 * Check all parameter types got determined.
@@ -1508,6 +1513,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 */
@@ -1805,6 +1812,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter corresponds to an encrypted column, but the parameter value was not encrypted")));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2602,8 +2619,47 @@ exec_describe_statement_message(const char *stmt_name)
 	{
 		Oid			ptype = psrc->param_types[i];
 
+		if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+		{
+			Oid			porigtbl = psrc->param_origtbls[i];
+			AttrNumber	porigcol = psrc->param_origcols[i];
+			HeapTuple   tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol <= 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("parameter %d corresponds to an encrypted column, but an underlying table and column could not be determined", i)));
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(porigtbl), Int16GetDatum(porigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", porigcol, porigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			ptype = orig_att->attrealtypid;
+
+			ReleaseSysCache(tp);
+		}
+
 		pq_sendint32(&row_description_buf, (int) ptype);
 	}
+
+	for (int i = 0; i < psrc->num_params; i++)
+	{
+		int16		pformat;
+
+		if (get_typtype(psrc->param_types[i]) == TYPTYPE_ENCRYPTED)
+		{
+			pformat = 0x10;
+			if (psrc->param_types[i] == ENCRYPTEDDOID)
+				pformat |= 0x20;
+		}
+		else
+			pformat = 0;
+
+		pq_sendint16(&row_description_buf, pformat);
+	}
+
 	pq_endmessage_reuse(&row_description_buf);
 
 	/*
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 6767eae8f2..869f37b26e 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -340,6 +340,8 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 				   MemoryContext querytree_context,
 				   Oid *param_types,
 				   int num_params,
+				   Oid *param_origtbls,
+				   AttrNumber *param_origcols,
 				   ParserSetupHook parserSetup,
 				   void *parserSetupArg,
 				   int cursor_options,
@@ -419,6 +421,14 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 	{
 		plansource->param_types = (Oid *) palloc(num_params * sizeof(Oid));
 		memcpy(plansource->param_types, param_types, num_params * sizeof(Oid));
+
+		plansource->param_origtbls = (Oid *) palloc0(num_params * sizeof(Oid));
+		if (param_origtbls)
+			memcpy(plansource->param_origtbls, param_origtbls, num_params * sizeof(Oid));
+
+		plansource->param_origcols = (AttrNumber *) palloc0(num_params * sizeof(AttrNumber));
+		if (param_origcols)
+			memcpy(plansource->param_origcols, param_origcols, num_params * sizeof(AttrNumber));
 	}
 	else
 		plansource->param_types = NULL;
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 56870b46e4..5c3f9b0055 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -29,7 +29,9 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -266,6 +268,24 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		256
 	},
+	{
+		ColumnEncKeyRelationId,	/* CEKNAME */
+		ColumnEncKeyNameIndexId,
+		1,
+		{
+			Anum_pg_colenckey_cekname,
+		},
+		8
+	},
+	{
+		ColumnEncKeyRelationId,	/* CEKOID */
+		ColumnEncKeyOidIndexId,
+		1,
+		{
+			Anum_pg_colenckey_oid,
+		},
+		8
+	},
 	{OperatorClassRelationId,	/* CLAAMNAMENSP */
 		OpclassAmNameNspIndexId,
 		3,
@@ -288,6 +308,15 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
+	{
+		ColumnMasterKeyRelationId,	/* CMKOID */
+		ColumnMasterKeyOidIndexId,
+		1,
+		{
+			Anum_pg_colmasterkey_oid,
+		},
+		8
+	},
 	{CollationRelationId,		/* COLLNAMEENCNSP */
 		CollationNameEncNspIndexId,
 		3,
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index c9b37538a0..90d8c93564 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,6 +20,8 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 8135854163..815cdac0af 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' },
 
+# encryptedd_ops
+{ amopfamily => 'hash/encryptedd_ops', amoplefttype => 'encryptedd',
+  amoprighttype => 'encryptedd', amopstrategy => '1', amopopr => '=(encryptedd,encryptedd)',
+  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 5460aa2422..a3698eea0f 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/encryptedd_ops', amproclefttype => 'encryptedd',
+  amprocrighttype => 'encryptedd', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/encryptedd_ops', amproclefttype => 'encryptedd',
+  amprocrighttype => 'encryptedd', 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 5c1ec9313e..48c774c053 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 */
+	int32		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 67f73cb6fb..22ff9d5c0b 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -548,4 +548,10 @@
 { castsource => 'tstzrange', casttarget => 'tstzmultirange',
   castfunc => 'tstzmultirange(tstzrange)', castcontext => 'e',
   castmethod => 'f' },
+
+{ castsource => 'bytea', casttarget => 'encryptedd', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+{ castsource => 'bytea', casttarget => 'encryptedr', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+
 ]
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..a59abc3a16
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,18 @@
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+} 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));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..cd19b0501b
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,25 @@
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+#ifdef CATALOG_VARLEN           /* variable-length fields start here */
+	text		ckdcmkalg BKI_FORCE_NOT_NULL;
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..15292b0b61
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,24 @@
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+#ifdef CATALOG_VARLEN           /* variable-length fields start here */
+	text		cmkprovider BKI_FORCE_NOT_NULL;
+	text		cmkoptions[1] 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 484727a2fc..d41e2aac2a 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 => 'encryptedd_ops', opcfamily => 'hash/encryptedd_ops',
+  opcintype => 'encryptedd' },
 { 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 0075a02f32..9fb9890522 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3475,4 +3475,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'encryptedd',
+  oprright => 'encryptedd', oprresult => 'bool', oprcom => '=(encryptedd,encryptedd)',
+  oprnegate => '<>(encryptedd,encryptedd)', oprcode => 'encrypteddeq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'encryptedd', oprright => 'encryptedd', oprresult => 'bool',
+  oprcom => '<>(encryptedd,encryptedd)', oprnegate => '=(encryptedd,encryptedd)',
+  oprcode => 'encrypteddne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index 8e480efd28..dcf9839e42 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 => 'encryptedd_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 79d787cd26..44a822aca5 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7992,9 +7992,9 @@
   proname => 'pg_prepared_statement', prorows => '1000', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'record',
   proargtypes => '',
-  proallargtypes => '{text,text,timestamptz,_regtype,bool,int8,int8}',
-  proargmodes => '{o,o,o,o,o,o,o}',
-  proargnames => '{name,statement,prepare_time,parameter_types,from_sql,generic_plans,custom_plans}',
+  proallargtypes => '{text,text,timestamptz,_regtype,bool,int8,int8,_regclass,_int2}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o}',
+  proargnames => '{name,statement,prepare_time,parameter_types,from_sql,generic_plans,custom_plans,parameter_orig_tables,parameter_orig_columns}',
   prosrc => 'pg_prepared_statement' },
 { oid => '2511', descr => 'get the open cursors for this session',
   proname => 'pg_cursor', prorows => '1000', proretset => 't',
@@ -11733,4 +11733,11 @@
   prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary',
   prosrc => 'brin_minmax_multi_summary_send' },
 
+{ oid => '8245',
+  proname => 'encrypteddeq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'encryptedd encryptedd', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'encrypteddne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'encryptedd encryptedd', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index 41074c994b..36e2e2c263 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 => 'encryptedd', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'x' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'encryptedr', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'x' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index e568e21dee..999720fb54 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -277,6 +277,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPTYPE_MULTIRANGE	'm' /* multirange type */
 #define  TYPTYPE_PSEUDO		'p' /* pseudo-type */
 #define  TYPTYPE_RANGE		'r' /* range type */
+#define  TYPTYPE_ENCRYPTED	'y'	/* encrypted column value */
 
 #define  TYPCATEGORY_INVALID	'\0'	/* not an allowed category */
 #define  TYPCATEGORY_ARRAY		'A'
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 067138e6b5..04e6e310ed 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -670,6 +670,7 @@ typedef struct ColumnDef
 	char	   *colname;		/* name of column */
 	TypeName   *typeName;		/* type of column */
 	char	   *compression;	/* compression method for column */
+	List	   *encryption;		/* encryption info for column */
 	int			inhcount;		/* number of times column is inherited */
 	bool		is_local;		/* column has local (non-inherited) def'n */
 	bool		is_not_null;	/* NOT NULL constraint specified? */
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index a0f0bd38d7..c1b8f7617a 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -27,7 +27,8 @@ extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook;
 extern Query *parse_analyze(RawStmt *parseTree, const char *sourceText,
 							Oid *paramTypes, int numParams, QueryEnvironment *queryEnv);
 extern Query *parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
-									  Oid **paramTypes, int *numParams);
+									  Oid **paramTypes, int *numParams,
+									  Oid **paramOrigTbls, AttrNumber **paramOrigCols);
 
 extern Query *parse_sub_analyze(Node *parseTree, ParseState *parentParseState,
 								CommonTableExpr *parentCTE,
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index ee179082ce..011162a63f 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -92,6 +92,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);
 
 
 /*
@@ -221,6 +222,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 b42fff296c..b90a315e9c 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -18,7 +18,8 @@
 extern void parse_fixed_parameters(ParseState *pstate,
 								   Oid *paramTypes, int numParams);
 extern void 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/utils/plancache.h b/src/include/utils/plancache.h
index ff09c63a02..eac90aa821 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -101,6 +101,8 @@ typedef struct CachedPlanSource
 	CommandTag	commandTag;		/* 'nuff said */
 	Oid		   *param_types;	/* array of parameter type OIDs, or NULL */
 	int			num_params;		/* length of param_types array */
+	Oid		   *param_origtbls;
+	AttrNumber *param_origcols;
 	ParserSetupHook parserSetup;	/* alternative parameter spec method */
 	void	   *parserSetupArg;
 	int			cursor_options; /* cursor options used for planning */
@@ -199,6 +201,8 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 							   MemoryContext querytree_context,
 							   Oid *param_types,
 							   int num_params,
+							   Oid *param_origtbls,
+							   AttrNumber *param_origcols,
 							   ParserSetupHook parserSetup,
 							   void *parserSetupArg,
 							   int cursor_options,
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index c8cfbc30f6..2420c9a818 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,11 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKNAME,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..7dacc93e0b 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,5 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPrepared2           187
+PQsendQueryPrepared2      188
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 9b6a6939f0..bc1b492557 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -4151,6 +4151,11 @@ freePGconn(PGconn *conn)
 		free(conn->gsslib);
 	if (conn->connip)
 		free(conn->connip);
+	if (conn->cekdata)
+	{
+		explicit_bzero(conn->cekdata, conn->cekdatalen);
+		free(conn->cekdata);
+	}
 	/* Note that conn->Pfdebug is not ours to close or free */
 	if (conn->write_err_msg)
 		free(conn->write_err_msg);
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 6c7b3df012..0e9431f360 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -28,6 +28,9 @@
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
 
+/* TODO: move OpenSSL-specific parts to separate file */
+#include <openssl/evp.h>
+
 /* keep this in same order as ExecStatusType in libpq-fe.h */
 char	   *const pgresStatus[] = {
 	"PGRES_EMPTY_QUERY",
@@ -65,7 +68,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1089,6 +1093,113 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+void
+pqSaveColumnMasterKey(PGconn *conn, const char *name, const char *provider,
+								  const char *optname1, const char *optval1)
+{
+	free(conn->cmkname);
+	conn->cmkname = strdup(name);
+	free(conn->cmkprovider);
+	conn->cmkprovider = strdup(provider);
+	free(conn->cmkoptname1);
+	conn->cmkoptname1 = strdup(optname1);
+	free(conn->cmkoptval1);
+	conn->cmkoptval1 = strdup(optval1);
+}
+
+/*
+ * a different key provider would replace this function
+ */
+static unsigned char *
+decrypt_cek_file(PGconn *conn,
+				 int fromlen, const unsigned char *from,
+				 int *tolen)
+{
+	RSA *rsa;
+	char *cmkfilename = conn->cmkoptval1;
+	FILE *fp;
+	unsigned char *to;
+	int rv;
+
+	/*
+	 * setup, load keys
+	 */
+
+	if (!cmkfilename)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("no CMK file set\n"));
+		goto fail;
+	}
+	rsa = RSA_new();
+	fp = fopen(cmkfilename, "rb");
+	if (!fp)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("could not open file \"%s\": %m\n"), cmkfilename);
+		goto fail;
+	}
+	rsa = PEM_read_RSAPrivateKey(fp, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("PEM_read_RSAPrivateKey() failed: %s\n"),
+						  ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	fclose(fp);
+
+	/*
+	 * decrypt
+	 */
+
+	to = malloc(RSA_size(rsa));
+
+	rv = RSA_private_decrypt(fromlen, from, to, rsa, RSA_PKCS1_OAEP_PADDING);
+	if (rv < 0)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("RSA_private_decrypt() failed: %s\n"),
+						  ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	RSA_free(rsa);
+
+	*tolen = rv;
+	return to;
+
+fail:
+	return false;
+}
+
+static unsigned char *
+decrypt_cek(PGconn *conn,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	if (strcmp(conn->cmkprovider, "file") == 0)
+		return decrypt_cek_file(conn, fromlen, from, tolen);
+	else
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("CMK provider \"%s\" not recognized\n"), conn->cmkprovider);
+		return NULL;
+	}
+}
+
+void
+pqSaveColumnEncryptionKey(PGconn *conn, const char *name, const unsigned char *value, int len)
+{
+	unsigned char *newval = NULL;
+	int newvallen = 0;
+
+	newval = decrypt_cek(conn, len, value, &newvallen);
+
+	free(conn->cekdata);
+	conn->cekdata = newval;
+	conn->cekdatalen = newvallen;
+}
 
 /*
  * pqRowProcessor
@@ -1153,9 +1264,103 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 		}
 		else
 		{
-			bool		isbinary = (res->attDescs[i].format != 0);
+			bool		isbinary = ((res->attDescs[i].format & 0x0F) != 0);
+			bool		isencrypted = ((res->attDescs[i].format & 0xF0) != 0);
 			char	   *val;
 
+			if (isencrypted)
+			{
+				unsigned char *v;
+				size_t l;
+				unsigned char *iv;
+				size_t ivlen;
+
+				EVP_CIPHER_CTX *evp_ctx;
+				const EVP_CIPHER *cipher;
+				unsigned char *decr;
+				int decrlen, decrlen2;
+
+				cipher = EVP_get_cipherbyname("AES-128-CBC");
+				if (!cipher)
+				{
+					*errmsgp = libpq_gettext("EVP_get_cipherbyname() failed");
+					goto fail;
+				}
+
+				evp_ctx = EVP_CIPHER_CTX_new();
+
+				if (!isbinary)
+				{
+					unsigned char *x;
+					x = malloc(clen + 1);
+					memcpy(x, columns[i].value, clen);
+					x[clen] = '\0';
+					v = PQunescapeBytea(x, &l);
+				}
+				else
+				{
+					v = unconstify(unsigned char *, (const unsigned char *) columns[i].value);
+					l = clen;
+				}
+
+				if (!EVP_DecryptInit_ex(evp_ctx, cipher, NULL, NULL, NULL))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptInit_ex() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+
+				if (conn->cekdatalen != EVP_CIPHER_CTX_key_length(evp_ctx))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("column encryption key has wrong key length for cipher (has: %zu, required: %d)"),
+							 conn->cekdatalen, EVP_CIPHER_CTX_key_length(evp_ctx));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+
+				ivlen = EVP_CIPHER_CTX_iv_length(evp_ctx);
+				iv = v;
+				v += ivlen;
+				l -= ivlen;
+				if (!EVP_DecryptInit_ex(evp_ctx, NULL, NULL, conn->cekdata, iv))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptInit_ex() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+
+				decr = malloc(l + EVP_CIPHER_CTX_block_size(evp_ctx) + 1);
+				if (!EVP_DecryptUpdate(evp_ctx, decr, &decrlen, v, l))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptUpdate() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+				if (!EVP_DecryptFinal_ex(evp_ctx, decr + decrlen, &decrlen2))
+				{
+					static char errmsg[100];
+					snprintf(errmsg, sizeof(errmsg), libpq_gettext("EVP_DecryptFinal_ex() failed: %s"),
+							ERR_reason_error_string(ERR_get_error()));
+					*errmsgp = errmsg;
+					goto fail;
+				}
+				decrlen += decrlen2;
+				decr[decrlen] = '\0';
+				val = (char *) decr;
+				clen = decrlen + 1;
+
+				EVP_CIPHER_CTX_free(evp_ctx);
+				free(iv);
+			}
+			else
+			{
 			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
 			if (val == NULL)
 				goto fail;
@@ -1163,6 +1368,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;
@@ -1470,7 +1676,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1588,6 +1795,19 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPrepared2(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+int
+PQsendQueryPrepared2(PGconn *conn,
+					 const char *stmtName,
+					 int nParams,
+					 const char *const *paramValues,
+					 const int *paramLengths,
+					 const int *paramFormats,
+					 int resultFormat,
+					 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1615,7 +1835,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1712,7 +1933,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1760,13 +1982,24 @@ PQsendQueryGuts(PGconn *conn,
 		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->format & 0x10)
+					format |= 0x10;
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1785,6 +2018,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1803,8 +2037,93 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].format & 0x10)
+			{
+				bool enc_det = (paramDesc->paramDescs[i].format & 0x20) != 0;
+				unsigned char *v;
+				unsigned char *iv;
+				size_t ivlen;
+				EVP_CIPHER_CTX *evp_ctx;
+				const EVP_CIPHER *cipher;
+				unsigned char *encr;
+				int encrlen, encrlen2;
+				unsigned char *esc;
+				size_t esclen;
+
+				v = unconstify(unsigned char *, (const unsigned char *) paramValue);
+
+				cipher = EVP_get_cipherbyname("AES-128-CBC");
+				if (!cipher)
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_get_cipherbyname() failed\n"));
+					goto sendFailed;
+				}
+
+				evp_ctx = EVP_CIPHER_CTX_new();
+
+				if (!EVP_EncryptInit_ex(evp_ctx, cipher, NULL, NULL, NULL))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptInit_ex() failed: %s\n"),
+									  ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+
+				if (conn->cekdatalen != EVP_CIPHER_CTX_key_length(evp_ctx))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("column encryption key has wrong key length for cipher (has: %zu, required: %d)"),
+									  conn->cekdatalen, EVP_CIPHER_CTX_key_length(evp_ctx));
+					goto sendFailed;
+				}
+
+				ivlen = EVP_CIPHER_CTX_iv_length(evp_ctx);
+				iv = malloc(ivlen);
+				if (enc_det)
+					memset(iv, ivlen, 0);
+				else
+					pg_strong_random(iv, ivlen);
+				if (!EVP_EncryptInit_ex(evp_ctx, NULL, NULL, conn->cekdata, iv))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptInit_ex() failed: %s\n"),
+									  ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+
+				encr = malloc(ivlen + nbytes + EVP_CIPHER_CTX_block_size(evp_ctx) + 1);
+				memcpy(encr, iv, ivlen);
+				encr += ivlen;
+				if (!EVP_EncryptUpdate(evp_ctx, encr, &encrlen, v, nbytes))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptUpdate() failed: %s\n"),
+													ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+				if (!EVP_EncryptFinal_ex(evp_ctx, encr + encrlen, &encrlen2))
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("EVP_EncryptFinal_ex() failed: %s\n"),
+									  ERR_reason_error_string(ERR_get_error()));
+					goto sendFailed;
+				}
+				encrlen += encrlen2;
+
+				EVP_CIPHER_CTX_free(evp_ctx);
+				free(iv);
+
+				esc = PQescapeByteaConn(conn, encr - ivlen, encrlen + ivlen, &esclen);
+				nbytes = esclen - 1;
+				paramValue = (char *) esc;
+			}
+
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
 		}
 		else
@@ -2258,12 +2577,25 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPrepared2(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+PGresult *
+PQexecPrepared2(PGconn *conn,
+				const char *stmtName,
+				int nParams,
+				const char *const *paramValues,
+				const int *paramLengths,
+				const int *paramFormats,
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
+	if (!PQsendQueryPrepared2(conn, stmtName,
 							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+							 paramFormats, resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 9ab3bf1fcb..c40550cdf6 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -45,6 +45,8 @@ static int	getRowDescriptions(PGconn *conn, int msgLength);
 static int	getParamDescriptions(PGconn *conn, int msgLength);
 static int	getAnotherTuple(PGconn *conn, int msgLength);
 static int	getParameterStatus(PGconn *conn);
+static int	getColumnMasterKey(PGconn *conn);
+static int	getColumnEncryptionKey(PGconn *conn);
 static int	getNotify(PGconn *conn);
 static int	getCopyStart(PGconn *conn, ExecStatusType copytype);
 static int	getReadyForQuery(PGconn *conn);
@@ -315,6 +317,14 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					if (getColumnMasterKey(conn))
+						return;
+					break;
+				case 'Y':		/* Column Encryption Key */
+					if (getColumnEncryptionKey(conn))
+						return;
+					break;
 				case 'T':		/* Row Description */
 					if (conn->result != NULL &&
 						conn->result->resultStatus == PGRES_FATAL_ERROR)
@@ -606,7 +616,7 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		result->attDescs[i].typlen = typlen;
 		result->attDescs[i].atttypmod = atttypmod;
 
-		if (format != 1)
+		if ((format & 0x0F) != 1)
 			result->binary = 0;
 	}
 
@@ -713,6 +723,14 @@ getParamDescriptions(PGconn *conn, int msgLength)
 			goto not_enough_data;
 		result->paramDescs[i].typid = typid;
 	}
+	for (i = 0; i < nparams; i++)
+	{
+		int			format;
+
+		if (pqGetInt(&format, 2, conn))
+			goto not_enough_data;
+		result->paramDescs[i].format = format;
+	}
 
 	/* Success! */
 	conn->result = result;
@@ -1429,6 +1447,69 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	char *keyname;
+	char *keyprov;
+	int noptions;
+	char *optname = NULL;
+	char *optval = NULL;
+
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	/* Get the key provider */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyprov = strdup(conn->workBuffer.data);
+	/* Get the options */
+	if (pqGetInt(&noptions, 2, conn) != 0)
+		return EOF;
+	for (int i = 0; i < noptions; i++)
+	{
+		if (pqGets(&conn->workBuffer, conn) != 0)
+			return EOF;
+		optname = strdup(conn->workBuffer.data);
+		if (pqGets(&conn->workBuffer, conn) != 0)
+			return EOF;
+		optval = strdup(conn->workBuffer.data);
+	}
+	/* And save it */
+	pqSaveColumnMasterKey(conn, keyname, keyprov, optname, optval);
+
+	free(keyname);
+	free(keyprov);
+	free(optname);
+	free(optval);
+	return 0;
+}
+
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	char *buf;
+	int vallen;
+
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, 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 (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	pqSaveColumnEncryptionKey(conn, conn->workBuffer.data, (unsigned char *) buf, vallen);
+	free(buf);
+	return 0;
+}
 
 /*
  * Attempt to read a Notify response message.
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index 8660d27926..e99a5d15af 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -459,6 +459,8 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 
 	for (int i = 0; i < nfields; i++)
 		pqTraceOutputInt32(f, message, cursor, regress);
+	for (int i = 0; i < nfields; i++)
+		pqTraceOutputInt16(f, message, cursor);
 }
 
 /* RowDescription */
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index a6fd69aceb..a6c8ec974a 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -436,6 +436,14 @@ 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,
+								 int resultFormat,
+								 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -459,6 +467,14 @@ 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,
+								 int resultFormat,
+								 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 490458adef..aa0f2eec2c 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -111,6 +111,7 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	int			format;			/* encrypted parameter? */
 } PGresParamDesc;
 
 /*
@@ -475,6 +476,14 @@ struct pg_conn
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
 
+	/* Encryption stuff */
+	char	   *cmkname;
+	char	   *cmkprovider;
+	char	   *cmkoptname1;	/* XXX only one option right now */
+	char	   *cmkoptval1;
+	unsigned char *cekdata;		/* column encryption key (decrypted) */
+	size_t		cekdatalen;
+
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
 	int			inBufSize;		/* allocated size of buffer */
@@ -648,6 +657,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern void pqSaveColumnMasterKey(PGconn *conn, const char *name, const char *provider,
+								  const char *optname1, const char *optval1);
+extern void pqSaveColumnEncryptionKey(PGconn *conn, const char *name,
+									  const unsigned char *value, int len);
 extern int	pqRowProcessor(PGconn *conn, const char **errmsgp);
 extern void pqCommandQueueAdvance(PGconn *conn);
 extern int	PQsendQueryContinue(PGconn *conn, const char *query);
diff --git a/src/test/Makefile b/src/test/Makefile
index 46275915ff..e2c819b047 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -28,6 +28,7 @@ SUBDIRS += ldap
 endif
 endif
 ifeq ($(with_ssl),openssl)
+SUBDIRS += column_encryption
 ifneq (,$(filter ssl,$(PG_TEST_EXTRA)))
 SUBDIRS += ssl
 endif
@@ -37,7 +38,7 @@ endif
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos ldap ssl column_encryption)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..1b9a5c72be
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,20 @@
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+override CPPFLAGS += -I$(libpq_srcdir)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+EXTRA_INSTALL = contrib/pgcrypto
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..b4eb7323b5
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,135 @@
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More tests => 13;
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+$node->safe_psql('postgres', qq{CREATE EXTENSION pgcrypto});
+
+my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/cmk1.pem";
+
+# generate CMK
+system_or_bail 'openssl', 'genrsa', '-out', $cmkfilename;
+
+# generate 16 random bytes for 128-bit key
+system_or_bail 'openssl', 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin", 16;
+
+# encrypt CEK using CMK
+system_or_bail 'openssl', 'rsautl', '-encrypt', '-oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin",
+  '-inkey', $cmkfilename,
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin.enc";
+
+# XXX might as well capture stdout, not go through file
+my $cekbin = slurp_file "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin";
+my $cekhex = unpack('H*', $cekbin);
+my $cekencbin = slurp_file "${PostgreSQL::Test::Utils::tmp_check}/cek1.bin.enc";
+my $cekenchex = unpack('H*', $cekencbin);
+
+# create CMK in database
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkprovider, cmkoptions) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    'cmk1', 'file', '{"filename", "$cmkfilename"}'::text[]
+);
+});
+
+# create CEK in database
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckey (oid, cekname) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    'cek1'
+);
+});
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    'RSA-OAEP',
+    '\\x${cekenchex}'
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+my $ivhex = '00' x 16;
+$node->safe_psql('postgres', qq{
+INSERT INTO tbl1 (a, b) VALUES (1, ('\\x${ivhex}'::bytea || encrypt_iv('val1', '\\x${cekhex}', '\\x${ivhex}', 'aes'))::encryptedr);
+INSERT INTO tbl1 (a, b) VALUES (2, ('\\x${ivhex}'::bytea || encrypt_iv('val2', '\\x${cekhex}', '\\x${ivhex}', 'aes'))::encryptedr);
+});
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'decrypted query result');
+
+
+$node->command_fails_like(['test_client', 'test1'], qr/not encrypted/, 'test client fails because parameters not encrypted');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'decrypted query result after test client insert');
+
+$node->command_ok(['test_client', 'test2'], 'test client test 2');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3),
+	'decrypted query result after test client insert 2');
+
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1 WHERE a = 3) TO STDOUT}),
+	qr/3\t\\\\x[0-9a-z]{32}/,
+	'inserted data is encrypted');
+
+$node->command_ok(['test_client', 'test3'], 'test client test 3');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3upd),
+	'decrypted query result after test client insert 3');
+
+$node->command_ok(['test_client', 'test4'], 'test client test 4');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result after test client insert 4');
+
+is($node->safe_psql('postgres', q{SELECT b, count(*) FROM tbl2 GROUP BY b ORDER BY 2}),
+	q(valB|1
+valA|2),
+	'group by deterministically encrypted column');
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..be37b51305
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,155 @@
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3"};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					   2, NULL, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res, *res2;
+	const char *values[] = {"3", "val3"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					2, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, 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, 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, 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
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace
index 1a7de5c3e6..ce84f1955b 100644
--- a/src/test/modules/libpq_pipeline/traces/prepared.trace
+++ b/src/test/modules/libpq_pipeline/traces/prepared.trace
@@ -2,7 +2,7 @@ F	68	Parse	 "select_one" "SELECT $1, '42', $1::numeric, interval '1 sec'" 1 NNNN
 F	16	Describe	 S "select_one"
 F	4	Sync
 B	4	ParseComplete
-B	10	ParameterDescription	 1 NNNN
+B	12	ParameterDescription	 1 NNNN 0
 B	113	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 65535 -1 0 "numeric" NNNN 0 NNNN 65535 -1 0 "interval" NNNN 0 NNNN 16 -1 0
 B	5	ReadyForQuery	 I
 F	10	Query	 "BEGIN"
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..1fec9f8903
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,47 @@
+/* Imagine:
+CREATE COLUMN MASTER KEY cmk1 (
+    provider = 'test'
+);
+*/
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkprovider, cmkoptions) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    'cmk1', 'test', '{}'::text[]
+);
+/* Imagine:
+CREATE COLUMN ENCRYPTION KEY cek1 (
+    column_master_key = cmk1,
+    algorithm = '...',
+    encrypted_value = '...'
+);
+*/
+INSERT INTO pg_colenckey (oid, cekname) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    'cek1'
+);
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    '2ROT13',
+    '\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      | encryptedr | default   |          | 
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..ab6405a6c3 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,5 @@ 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_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 562b586d8e..fc2a07971d 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                       | encryptedd
  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,11 +198,12 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | encryptedd
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(4 rows)
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -840,6 +842,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+encrypteddeq(encryptedd,encryptedd)
+encrypteddne(encryptedd,encryptedd)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
@@ -987,7 +991,9 @@ WHERE c.castmethod = 'b' AND
  xml               | text              |        0 | a
  xml               | character varying |        0 | a
  xml               | character         |        0 | a
-(10 rows)
+ bytea             | encryptedd        |        0 | e
+ bytea             | encryptedr        |        0 | e
+(12 rows)
 
 -- **************** pg_conversion ****************
 -- Look for illegal values in pg_conversion fields.
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 3306c696b1..675556c7d1 100644
--- a/src/test/regress/expected/prepare.out
+++ b/src/test/regress/expected/prepare.out
@@ -159,25 +159,30 @@ PREPARE q6 AS
     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;
 PREPARE q7(unknown) AS
     SELECT * FROM road WHERE thepath = $1;
-SELECT name, statement, parameter_types FROM pg_prepared_statements
+-- DML statements
+PREPARE q8 AS
+    UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;
+SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns FROM pg_prepared_statements
     ORDER BY name;
- name |                            statement                             |                  parameter_types                   
-------+------------------------------------------------------------------+----------------------------------------------------
- q2   | PREPARE q2(text) AS                                             +| {text}
-      |         SELECT datname, datistemplate, datallowconn             +| 
-      |         FROM pg_database WHERE datname = $1;                     | 
- q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint}
-      |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+| 
-      |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +| 
-      |         ORDER BY unique1;                                        | 
- q5   | PREPARE q5(int, text) AS                                        +| {integer,text}
-      |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +| 
-      |         ORDER BY unique1;                                        | 
- q6   | PREPARE q6 AS                                                   +| {integer,name}
-      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    | 
- q7   | PREPARE q7(unknown) AS                                          +| {path}
-      |     SELECT * FROM road WHERE thepath = $1;                       | 
-(5 rows)
+ name |                            statement                             |                  parameter_types                   | parameter_orig_tables | parameter_orig_columns 
+------+------------------------------------------------------------------+----------------------------------------------------+-----------------------+------------------------
+ q2   | PREPARE q2(text) AS                                             +| {text}                                             | {-}                   | {0}
+      |         SELECT datname, datistemplate, datallowconn             +|                                                    |                       | 
+      |         FROM pg_database WHERE datname = $1;                     |                                                    |                       | 
+ q3   | PREPARE q3(text, int, float, boolean, smallint) AS              +| {text,integer,"double precision",boolean,smallint} | {-,-,-,-,-}           | {0,0,0,0,0}
+      |         SELECT * FROM tenk1 WHERE string4 = $1 AND (four = $2 OR+|                                                    |                       | 
+      |         ten = $3::bigint OR true = $4 OR odd = $5::int)         +|                                                    |                       | 
+      |         ORDER BY unique1;                                        |                                                    |                       | 
+ q5   | PREPARE q5(int, text) AS                                        +| {integer,text}                                     | {-,-}                 | {0,0}
+      |         SELECT * FROM tenk1 WHERE unique1 = $1 OR stringu1 = $2 +|                                                    |                       | 
+      |         ORDER BY unique1;                                        |                                                    |                       | 
+ q6   | PREPARE q6 AS                                                   +| {integer,name}                                     | {-,-}                 | {0,0}
+      |     SELECT * FROM tenk1 WHERE unique1 = $1 AND stringu1 = $2;    |                                                    |                       | 
+ q7   | PREPARE q7(unknown) AS                                          +| {path}                                             | {-}                   | {0}
+      |     SELECT * FROM road WHERE thepath = $1;                       |                                                    |                       | 
+ q8   | PREPARE q8 AS                                                   +| {integer,name}                                     | {-,tenk1}             | {0,14}
+      |     UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;           |                                                    |                       | 
+(6 rows)
 
 -- test DEALLOCATE ALL;
 DEALLOCATE ALL;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index b58b062b10..7883baf4ab 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1441,8 +1441,10 @@ pg_prepared_statements| SELECT p.name,
     p.parameter_types,
     p.from_sql,
     p.generic_plans,
-    p.custom_plans
-   FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, from_sql, generic_plans, custom_plans);
+    p.custom_plans,
+    p.parameter_orig_tables,
+    p.parameter_orig_columns
+   FROM pg_prepared_statement() p(name, statement, prepare_time, parameter_types, from_sql, generic_plans, custom_plans, parameter_orig_tables, parameter_orig_columns);
 pg_prepared_xacts| SELECT p.transaction,
     p.gid,
     p.prepared,
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 63706a28cc..67a0fcc9a3 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -112,7 +112,10 @@ pg_auth_members|t
 pg_authid|t
 pg_cast|t
 pg_class|t
+pg_colenckey|t
+pg_colenckeydata|t
 pg_collation|t
+pg_colmasterkey|t
 pg_constraint|t
 pg_conversion|t
 pg_database|t
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 257b6cac12..403e9f5c1e 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -17,7 +17,7 @@ SELECT p1.oid, p1.typname
 FROM pg_type as p1
 WHERE p1.typnamespace = 0 OR
     (p1.typlen <= 0 AND p1.typlen != -1 AND p1.typlen != -2) OR
-    (p1.typtype not in ('b', 'c', 'd', 'e', 'p', 'r', 'm')) OR
+    (p1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT p1.typisdefined OR
     (p1.typalign not in ('c', 's', 'i', 'd')) OR
     (p1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -75,7 +75,9 @@ ORDER BY p1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | encryptedd
+ 8244 | encryptedr
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT p1.oid, p1.typname as basetype, p2.typname as arraytype,
@@ -210,7 +212,8 @@ ORDER BY 1;
  e       | enum_in
  m       | multirange_in
  r       | range_in
-(5 rows)
+ y       | byteain
+(6 rows)
 
 -- Check for bogus typoutput routines
 -- As of 8.0, this check finds refcursor, which is borrowing
@@ -255,7 +258,8 @@ ORDER BY 1;
  e       | enum_out
  m       | multirange_out
  r       | range_out
-(4 rows)
+ y       | byteaout
+(5 rows)
 
 -- Domains should have same typoutput as their base types
 SELECT p1.oid, p1.typname, p2.oid, p2.typname
@@ -335,7 +339,8 @@ ORDER BY 1;
  e       | enum_recv
  m       | multirange_recv
  r       | range_recv
-(5 rows)
+ y       | bytearecv
+(6 rows)
 
 -- Check for bogus typsend routines
 -- As of 7.4, this check finds refcursor, which is borrowing
@@ -380,7 +385,8 @@ ORDER BY 1;
  e       | enum_send
  m       | multirange_send
  r       | range_send
-(4 rows)
+ y       | byteasend
+(5 rows)
 
 -- Domains should have same typsend as their base types
 SELECT p1.oid, p1.typname, p2.oid, p2.typname
@@ -707,6 +713,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::encryptedr,
+  E'\\xDEADBEEF'::encryptedd,
   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 017e962fed..cfc7c4e69a 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,6 +123,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
 
+# WIP
+test: column_encryption
+
 # event triggers 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/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..745d6c2ede
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,44 @@
+/* Imagine:
+CREATE COLUMN MASTER KEY cmk1 (
+    provider = 'test'
+);
+*/
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkprovider, cmkoptions) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    'cmk1', 'test', '{}'::text[]
+);
+
+/* Imagine:
+CREATE COLUMN ENCRYPTION KEY cek1 (
+    column_master_key = cmk1,
+    algorithm = '...',
+    encrypted_value = '...'
+);
+*/
+INSERT INTO pg_colenckey (oid, cekname) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    'cek1'
+);
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    '2ROT13',
+    '\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
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index 985d0f05c9..b2aa96d370 100644
--- a/src/test/regress/sql/prepare.sql
+++ b/src/test/regress/sql/prepare.sql
@@ -71,7 +71,11 @@ CREATE TEMPORARY TABLE q5_prep_nodata AS EXECUTE q5(200, 'DTAAAA')
 PREPARE q7(unknown) AS
     SELECT * FROM road WHERE thepath = $1;
 
-SELECT name, statement, parameter_types FROM pg_prepared_statements
+-- DML statements
+PREPARE q8 AS
+    UPDATE tenk1 SET stringu1 = $2 WHERE unique1 = $1;
+
+SELECT name, statement, parameter_types, parameter_orig_tables, parameter_orig_columns FROM pg_prepared_statements
     ORDER BY name;
 
 -- test DEALLOCATE ALL;
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 8281076423..f31f9da139 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -20,7 +20,7 @@
 FROM pg_type as p1
 WHERE p1.typnamespace = 0 OR
     (p1.typlen <= 0 AND p1.typlen != -1 AND p1.typlen != -2) OR
-    (p1.typtype not in ('b', 'c', 'd', 'e', 'p', 'r', 'm')) OR
+    (p1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT p1.typisdefined OR
     (p1.typalign not in ('c', 's', 'i', 'd')) OR
     (p1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -529,6 +529,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::encryptedr,
+  E'\\xDEADBEEF'::encryptedd,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,

base-commit: 49422ad0cc88c91a38522b2a7b222c2f2c939f82
-- 
2.34.1

#2Robert Haas
robertmhaas@gmail.com
In reply to: Peter Eisentraut (#1)
Re: Transparent column encryption

On Fri, Dec 3, 2021 at 4:32 PM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

But it's missing the remaining 90% of the work,
including additional DDL support, error handling, robust memory
management, protocol versioning, forward and backward compatibility,
pg_dump support, psql \d support, refinement of the cryptography, and
so on. But I think obvious solutions exist to all of those things, so
it isn't that interesting to focus on them for now.

Right, we wouldn't want to get bogged down at this stage in little
details like, uh, everything.

Some protocol extensions are required. These should be guarded by
some _pq_... setting, but this is not done in this patch yet. As
mentioned above, extra messages are added for sending the CMKs and
CEKs. In the RowDescription message, I have commandeered the format
field to add a bit that indicates that the field is encrypted. This
could be made a separate field, and there should probably be
additional fields to indicate the algorithm and CEK name, but this was
easiest for now. The ParameterDescription message is extended to
contain format fields for each parameter, for the same purpose.
Again, this could be done differently.

I think this is reasonable. I would choose to use an additional bit in
the format field as opposed to a separate field. It is worth
considering whether it makes more sense to extend the existing
ParameterDescription message conditionally on some protocol-level
option, or whether we should instead, say, add ParameterDescription2
or the moral equivalent. As I see it, the latter feels conceptually
simpler, but on the other hand, our wire protocol supposes that we
will never run out of 1-byte codes for messages, so perhaps some
prudence is needed.

Speaking of parameter descriptions, 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.

Why? If the client knows the CEK, can't the client choose to send
unprepared insert or update statements with pre-encrypted blobs? That
might be a bad idea from a security perspective because the encrypted
blob might then got logged, but we sometimes log parameters, too.

--
Robert Haas
EDB: http://www.enterprisedb.com

#3Jacob Champion
pchampion@vmware.com
In reply to: Peter Eisentraut (#1)
Re: Transparent column encryption

On Fri, 2021-12-03 at 22:32 +0100, Peter Eisentraut wrote:

This feature does support deterministic
encryption as an alternative to the default randomized encryption, so
in that mode you can do equality lookups, at the cost of some
security.

+ if (enc_det)
+ memset(iv, ivlen, 0);

I think reusing a zero IV will potentially leak more information than
just equality, depending on the cipher in use. You may be interested in
synthetic IVs and nonce-misuse resistance (e.g. [1]https://datatracker.ietf.org/doc/html/rfc8452), since they seem
like they would match this use case exactly. (But I'm not a
cryptographer.)

The encryption algorithms are mostly hardcoded right now, but there
are facilities for picking algorithms and adding new ones that will be
expanded. The CMK process uses RSA-OAEP. The CEK process uses
AES-128-CBC right now; a more complete solution should probably
involve some HMAC thrown in.

Have you given any thought to AEAD? As a client I'd like to be able to
tie an encrypted value to other column (or external) data. For example,
AEAD could be used to prevent a DBA from copying the (encrypted) value
of my credit card column into their account's row to use it.

This is not targeting PostgreSQL 15. But I'd appreciate some feedback
on the direction.

What kinds of attacks are you hoping to prevent (and not prevent)?

--Jacob

[1]: https://datatracker.ietf.org/doc/html/rfc8452

#4Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Robert Haas (#2)
Re: Transparent column encryption

On 06.12.21 19:28, Robert Haas wrote:

Speaking of parameter descriptions, 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.

Why? If the client knows the CEK, can't the client choose to send
unprepared insert or update statements with pre-encrypted blobs? That
might be a bad idea from a security perspective because the encrypted
blob might then got logged, but we sometimes log parameters, too.

The client can send something like

PQexec(conn, "INSERT INTO tbl VALUES ('ENCBLOB', 'ENCBLOB')");

and it will work. (See the included test suite where 'ENCBLOB' is
actually computed by pgcrypto.) But that is not transparent encryption.
The client wants to send "INSERT INTO tbl VALUES ('val1', 'val2')" and
have libpq take care of encrypting 'val1' and 'val2' before hitting the
wire. For that you need to use the prepared statement API so that the
values are available separately from the statement. And furthermore the
client needs to know what columns the insert statements is writing to,
so that it can get the CEK for that column. That's what it needs the
parameter description for.

As alluded to, workarounds exist or might be made available to do part
of that work yourself, but that shouldn't be the normal way of using it.

#5Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#3)
Re: Transparent column encryption

On 06.12.21 21:44, Jacob Champion wrote:

I think reusing a zero IV will potentially leak more information than
just equality, depending on the cipher in use. You may be interested in
synthetic IVs and nonce-misuse resistance (e.g. [1]), since they seem
like they would match this use case exactly. (But I'm not a
cryptographer.)

I'm aware of this and plan to make use of SIV. The current
implementation is just an example.

Have you given any thought to AEAD? As a client I'd like to be able to
tie an encrypted value to other column (or external) data. For example,
AEAD could be used to prevent a DBA from copying the (encrypted) value
of my credit card column into their account's row to use it.

I don't know how that is supposed to work. When the value is encrypted
for insertion, the client may know things like table name or column
name, so it can tie it to those. But it doesn't know what row it will
go in, so you can't prevent the value from being copied into another
row. You would need some permanent logical row ID for this, I think.
For this scenario, the deterministic encryption mode is perhaps not the
right one.

This is not targeting PostgreSQL 15. But I'd appreciate some feedback
on the direction.

What kinds of attacks are you hoping to prevent (and not prevent)?

The point is to prevent admins from getting at plaintext data. The
scenario you show is an interesting one but I think it's not meant to be
addressed by this. If admins can alter the database to their advantage,
they could perhaps increase their account balance, create discount
codes, etc. also.

If this is a problem, then perhaps a better approach would be to store
parts of the data in a separate database with separate admins.

#6Jacob Champion
pchampion@vmware.com
In reply to: Peter Eisentraut (#5)
Re: Transparent column encryption

On Tue, 2021-12-07 at 16:39 +0100, Peter Eisentraut wrote:

On 06.12.21 21:44, Jacob Champion wrote:

I think reusing a zero IV will potentially leak more information than
just equality, depending on the cipher in use. You may be interested in
synthetic IVs and nonce-misuse resistance (e.g. [1]), since they seem
like they would match this use case exactly. (But I'm not a
cryptographer.)

I'm aware of this and plan to make use of SIV. The current
implementation is just an example.

Sounds good.

Have you given any thought to AEAD? As a client I'd like to be able to
tie an encrypted value to other column (or external) data. For example,
AEAD could be used to prevent a DBA from copying the (encrypted) value
of my credit card column into their account's row to use it.

I don't know how that is supposed to work. When the value is encrypted
for insertion, the client may know things like table name or column
name, so it can tie it to those. But it doesn't know what row it will
go in, so you can't prevent the value from being copied into another
row. You would need some permanent logical row ID for this, I think.

Sorry, my description was confusing. There's nothing preventing the DBA
from copying the value inside the database, but AEAD can make it so
that the copied value isn't useful to the DBA.

Sample case. Say I have a webapp backed by Postgres, which stores
encrypted credit card numbers. Users authenticate to the webapp which
then uses the client (which has the keys) to talk to the database.
Additionally, I assume that:

- the DBA can't access the client directly (because if they can, then
they can unencrypt the victim's info using the client's keys), and

- the DBA can't authenticate as the user/victim (because if they can,
they can just log in themselves and have the data). The webapp might
for example use federated authn with a separate provider, using an
email address as an identifier.

Now, if the client encrypts a user's credit card number using their
email address as associated data, then it doesn't matter if the DBA
copies that user's encrypted card over to their own account. The DBA
can't log in as the victim, so the client will fail to authenticate the
value because its associated data won't match.

This is not targeting PostgreSQL 15. But I'd appreciate some feedback
on the direction.

What kinds of attacks are you hoping to prevent (and not prevent)?

The point is to prevent admins from getting at plaintext data. The
scenario you show is an interesting one but I think it's not meant to be
addressed by this. If admins can alter the database to their advantage,
they could perhaps increase their account balance, create discount
codes, etc. also.

Sure, but increasing account balances and discount codes don't lead to
getting at plaintext data, right? Whereas stealing someone else's
encrypted value seems like it would be covered under your threat model,
since it lets you trick a real-world client into decrypting it for you.

Other avenues of attack might depend on how you choose to add HMAC to
the current choice of AES-CBC. My understanding of AE ciphers (with or
without associated data) is that you don't have to design that
yourself, which is nice.

--Jacob

#7Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Jacob Champion (#6)
Re: Transparent column encryption

On 12/7/21 19:02, Jacob Champion wrote:

On Tue, 2021-12-07 at 16:39 +0100, Peter Eisentraut wrote:

On 06.12.21 21:44, Jacob Champion wrote:

I think reusing a zero IV will potentially leak more information than
just equality, depending on the cipher in use. You may be interested in
synthetic IVs and nonce-misuse resistance (e.g. [1]), since they seem
like they would match this use case exactly. (But I'm not a
cryptographer.)

I'm aware of this and plan to make use of SIV. The current
implementation is just an example.

Sounds good.

Have you given any thought to AEAD? As a client I'd like to be able to
tie an encrypted value to other column (or external) data. For example,
AEAD could be used to prevent a DBA from copying the (encrypted) value
of my credit card column into their account's row to use it.

I don't know how that is supposed to work. When the value is encrypted
for insertion, the client may know things like table name or column
name, so it can tie it to those. But it doesn't know what row it will
go in, so you can't prevent the value from being copied into another
row. You would need some permanent logical row ID for this, I think.

Sorry, my description was confusing. There's nothing preventing the DBA
from copying the value inside the database, but AEAD can make it so
that the copied value isn't useful to the DBA.

Sample case. Say I have a webapp backed by Postgres, which stores
encrypted credit card numbers. Users authenticate to the webapp which
then uses the client (which has the keys) to talk to the database.
Additionally, I assume that:

- the DBA can't access the client directly (because if they can, then
they can unencrypt the victim's info using the client's keys), and

- the DBA can't authenticate as the user/victim (because if they can,
they can just log in themselves and have the data). The webapp might
for example use federated authn with a separate provider, using an
email address as an identifier.

Now, if the client encrypts a user's credit card number using their
email address as associated data, then it doesn't matter if the DBA
copies that user's encrypted card over to their own account. The DBA
can't log in as the victim, so the client will fail to authenticate the
value because its associated data won't match.

This is not targeting PostgreSQL 15. But I'd appreciate some feedback
on the direction.

What kinds of attacks are you hoping to prevent (and not prevent)?

The point is to prevent admins from getting at plaintext data. The
scenario you show is an interesting one but I think it's not meant to be
addressed by this. If admins can alter the database to their advantage,
they could perhaps increase their account balance, create discount
codes, etc. also.

Sure, but increasing account balances and discount codes don't lead to
getting at plaintext data, right? Whereas stealing someone else's
encrypted value seems like it would be covered under your threat model,
since it lets you trick a real-world client into decrypting it for you.

Other avenues of attack might depend on how you choose to add HMAC to
the current choice of AES-CBC. My understanding of AE ciphers (with or
without associated data) is that you don't have to design that
yourself, which is nice.

IMO it's impossible to solve this attack within TCE, because it requires
ensuring consistency at the row level, but TCE obviously works at column
level only.

I believe TCE can do AEAD at the column level, which protects against
attacks that flipping bits, and similar attacks. It's just a matter of
how the client encrypts the data.

Extending it to protect the whole row seems tricky, because the client
may not even know the other columns, and it's not clear to me how it'd
deal with things like updates of the other columns, hint bits, dropped
columns, etc.

It's probably possible to get something like this (row-level AEAD) by
encrypting enriched data, i.e. not just the card number, but {user ID,
card number} or something like that, and verify that in the webapp. The
problem of course is that the "user ID" is just another column in the
table, and there's nothing preventing the DBA from modifying that too.

So I think it's pointless to try extending this to row-level AEAD.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#8Jacob Champion
pchampion@vmware.com
In reply to: Tomas Vondra (#7)
Re: Transparent column encryption

On Tue, 2021-12-07 at 22:21 +0100, Tomas Vondra wrote:

IMO it's impossible to solve this attack within TCE, because it requires
ensuring consistency at the row level, but TCE obviously works at column
level only.

I was under the impression that clients already had to be modified to
figure out how to encrypt the data? If part of that process ends up
including enforcement of encryption for a specific column set, then the
addition of AEAD data could hypothetically be part of that hand-
waviness.

Unless "transparent" means that the client completely defers to the
server on whether to encrypt or not, and silently goes along with it if
the server tells it not to encrypt? That would only protect against a
_completely_ passive DBA, like someone reading unencrypted backups,
etc. And that still has a lot of value, certainly. But it seems like
this prototype is very close to a system where the client can reliably
secure data even if the server isn't trustworthy, if that's a use case
you're interested in.

I believe TCE can do AEAD at the column level, which protects against
attacks that flipping bits, and similar attacks. It's just a matter of
how the client encrypts the data.

Right, I think authenticated encryption ciphers (without AD) will be
important to support in practice. I think users are going to want
*some* protection against active attacks.

Extending it to protect the whole row seems tricky, because the client
may not even know the other columns, and it's not clear to me how it'd
deal with things like updates of the other columns, hint bits, dropped
columns, etc.

Covering the entire row automatically probably isn't super helpful in
practice. As you mention later:

It's probably possible to get something like this (row-level AEAD) by
encrypting enriched data, i.e. not just the card number, but {user ID,
card number} or something like that, and verify that in the webapp. The
problem of course is that the "user ID" is just another column in the
table, and there's nothing preventing the DBA from modifying that too.

Right. That's why the client has to be able to choose AD according to
the application. In my previous example, the victim's email address can
be copied by the DBA, but they wouldn't be able to authenticate as that
user and couldn't convince the client to use the plaintext on their
behalf.

--Jacob

#9Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Jacob Champion (#8)
Re: Transparent column encryption

On 12/8/21 00:26, Jacob Champion wrote:

On Tue, 2021-12-07 at 22:21 +0100, Tomas Vondra wrote:

IMO it's impossible to solve this attack within TCE, because it requires
ensuring consistency at the row level, but TCE obviously works at column
level only.

I was under the impression that clients already had to be modified to
figure out how to encrypt the data? If part of that process ends up
including enforcement of encryption for a specific column set, then the
addition of AEAD data could hypothetically be part of that hand-
waviness.

I think "transparency" here means the client just uses the regular
prepared-statement API without having to explicitly encrypt/decrypt any
data. The problem is we can't easily tie this to other columns in the
table, because the client may not even know what values are in those
columns.

Imagine you do this

UPDATE t SET encrypted_column = $1 WHERE another_column = $2;

but you want to ensure the encrypted value belongs to a particular row
(which may or may not be identified by the another_column value). How
would the client do that? Should it fetch the value or what?

Similarly, what if the client just does

SELECT encrypted_column FROM t;

How would it verify the values belong to the row, without having all the
data for the row (or just the required columns)?

Unless "transparent" means that the client completely defers to the
server on whether to encrypt or not, and silently goes along with it if
the server tells it not to encrypt?

I think that's probably a valid concern - a "bad DBA" could alter the
table definition to not contain the "ENCRYPTED" bits, and then peek at
the plaintext values.

But it's not clear to me how exactly would the AEAD prevent this?
Wouldn't that be also specified on the server, somehow? In which case
the DBA could just tweak that too, no?

In other words, this issue seems mostly orthogonal to the AEAD, and the
right solution would be to allow the client to define which columns have
to be encrypted (in which case altering the server definition would not
be enough).

That would only protect against a
_completely_ passive DBA, like someone reading unencrypted backups,
etc. And that still has a lot of value, certainly. But it seems like
this prototype is very close to a system where the client can reliably
secure data even if the server isn't trustworthy, if that's a use case
you're interested in.

Right. IMHO the "passive attacker" is a perfectly fine model for use
cases that would be fine with e.g. pgcrypto if there was no risk of
leaking plaintext values to logs, system catalogs, etc.

If we can improve it to provide (at least some) protection against
active attackers, that'd be a nice bonus.

I believe TCE can do AEAD at the column level, which protects against
attacks that flipping bits, and similar attacks. It's just a matter of
how the client encrypts the data.

Right, I think authenticated encryption ciphers (without AD) will be
important to support in practice. I think users are going to want
*some* protection against active attacks.

Extending it to protect the whole row seems tricky, because the client
may not even know the other columns, and it's not clear to me how it'd
deal with things like updates of the other columns, hint bits, dropped
columns, etc.

Covering the entire row automatically probably isn't super helpful in
practice. As you mention later:

It's probably possible to get something like this (row-level AEAD) by
encrypting enriched data, i.e. not just the card number, but {user ID,
card number} or something like that, and verify that in the webapp. The
problem of course is that the "user ID" is just another column in the
table, and there's nothing preventing the DBA from modifying that too.

Right. That's why the client has to be able to choose AD according to
the application. In my previous example, the victim's email address can
be copied by the DBA, but they wouldn't be able to authenticate as that
user and couldn't convince the client to use the plaintext on their
behalf.

Well, yeah. But I'm not sure how to make that work easily, because the
client may not have the data :-(

I was thinking about using a composite data type combining the data with
the extra bits - that'd not be all that transparent as it'd require the
client to build this manually and then also cross-check it after loading
the data. So the user would be responsible for having all the data.

But doing that automatically/transparently seems hard, because how would
you deal e.g. with SELECT queries reading data through a view or CTE?

How would you declare this, either at the client or server?

Do any other databases have this capability? How do they do it?

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#10Jacob Champion
pchampion@vmware.com
In reply to: Tomas Vondra (#9)
Re: Transparent column encryption

On Wed, 2021-12-08 at 02:58 +0100, Tomas Vondra wrote:

On 12/8/21 00:26, Jacob Champion wrote:

On Tue, 2021-12-07 at 22:21 +0100, Tomas Vondra wrote:

IMO it's impossible to solve this attack within TCE, because it requires
ensuring consistency at the row level, but TCE obviously works at column
level only.

I was under the impression that clients already had to be modified to
figure out how to encrypt the data? If part of that process ends up
including enforcement of encryption for a specific column set, then the
addition of AEAD data could hypothetically be part of that hand-
waviness.

I think "transparency" here means the client just uses the regular
prepared-statement API without having to explicitly encrypt/decrypt any
data. The problem is we can't easily tie this to other columns in the
table, because the client may not even know what values are in those
columns.

The way I originally described my request -- "I'd like to be able to
tie an encrypted value to other column (or external) data" -- was not
very clear.

With my proposed model -- where the DBA (and the server) are completely
untrusted, and the DBA needs to be prevented from using the encrypted
value -- I don't think there's a useful way for the client to use
associated data that comes from the server. The client has to know what
the AD should be beforehand, because otherwise the DBA can make it so
the server returns whatever is correct.

Imagine you do this

UPDATE t SET encrypted_column = $1 WHERE another_column = $2;

but you want to ensure the encrypted value belongs to a particular row
(which may or may not be identified by the another_column value). How
would the client do that? Should it fetch the value or what?

Similarly, what if the client just does

SELECT encrypted_column FROM t;

How would it verify the values belong to the row, without having all the
data for the row (or just the required columns)?

So with my (hopefully more clear) model above, it wouldn't. The client
would already have the AD, and somehow tell libpq what that data was
for the query.

The rabbit hole I led you down is one where we use the rest of the row
as AD, to try to freeze pieces of it in place. That might(?) have some
useful security properties (if the client defines its use and doesn't
defer to the server). But it's not what I intended to propose and I'd
have to think about that case some more.

In my credit card example, I'm imagining something like (forgive the
contrived syntax):

SELECT address, :{aead(users.credit_card, 'user@example.com')}
FROM users WHERE email = 'user@example.com';

UPDATE users
SET :{aead(users.credit_card, 'user@example.com')} = '1234-...'
WHERE email = 'user@example.com';

The client explicitly links a table's column to its AD for the duration
of the query. This approach can't scale to

SELECT credit_card FROM users;

because in this case the AD for each row is different, but I'd argue
that's ideal for this particular case. The client doesn't need to (and
probably shouldn't) grab everyone's credit card details all at once, so
there's no reason to optimize for it.

Unless "transparent" means that the client completely defers to the
server on whether to encrypt or not, and silently goes along with it if
the server tells it not to encrypt?

I think that's probably a valid concern - a "bad DBA" could alter the
table definition to not contain the "ENCRYPTED" bits, and then peek at
the plaintext values.

But it's not clear to me how exactly would the AEAD prevent this?
Wouldn't that be also specified on the server, somehow? In which case
the DBA could just tweak that too, no?

In other words, this issue seems mostly orthogonal to the AEAD, and the
right solution would be to allow the client to define which columns have
to be encrypted (in which case altering the server definition would not
be enough).

Right, exactly. When I mentioned AEAD I had assumed that "allow the
client to define which columns have to be encrypted" was already
planned or in the works; I just misunderstood pieces of Peter's email.
It's that piece where a client would probably have to add details
around AEAD and its use.

That would only protect against a
_completely_ passive DBA, like someone reading unencrypted backups,
etc. And that still has a lot of value, certainly. But it seems like
this prototype is very close to a system where the client can reliably
secure data even if the server isn't trustworthy, if that's a use case
you're interested in.

Right. IMHO the "passive attacker" is a perfectly fine model for use
cases that would be fine with e.g. pgcrypto if there was no risk of
leaking plaintext values to logs, system catalogs, etc.

If we can improve it to provide (at least some) protection against
active attackers, that'd be a nice bonus.

I agree that resistance against offline attacks is a useful step
forward (it seems to be a strict improvement over pgcrypto). I have a
feeling that end users will *expect* some protection against online
attacks too, since an evil DBA is going to be well-positioned to do
exactly that.

It's probably possible to get something like this (row-level AEAD) by
encrypting enriched data, i.e. not just the card number, but {user ID,
card number} or something like that, and verify that in the webapp. The
problem of course is that the "user ID" is just another column in the
table, and there's nothing preventing the DBA from modifying that too.

Right. That's why the client has to be able to choose AD according to
the application. In my previous example, the victim's email address can
be copied by the DBA, but they wouldn't be able to authenticate as that
user and couldn't convince the client to use the plaintext on their
behalf.

Well, yeah. But I'm not sure how to make that work easily, because the
client may not have the data :-(

I was thinking about using a composite data type combining the data with
the extra bits - that'd not be all that transparent as it'd require the
client to build this manually and then also cross-check it after loading
the data. So the user would be responsible for having all the data.

But doing that automatically/transparently seems hard, because how would
you deal e.g. with SELECT queries reading data through a view or CTE?

How would you declare this, either at the client or server?

I'll do some more thinking on the case you're talking about here, where
pieces of the row are transparently tied together.

Do any other databases have this capability? How do they do it?

BigQuery advertises AEAD support. I don't think their model is the same
as ours, though; from the docs it looks like it's essentially pgcrypto,
where you tell the server to encrypt stuff for you.

--Jacob

#11Tomas Vondra
tomas.vondra@enterprisedb.com
In reply to: Jacob Champion (#10)
Re: Transparent column encryption

On 12/9/21 01:12, Jacob Champion wrote:

On Wed, 2021-12-08 at 02:58 +0100, Tomas Vondra wrote:

On 12/8/21 00:26, Jacob Champion wrote:

On Tue, 2021-12-07 at 22:21 +0100, Tomas Vondra wrote:

IMO it's impossible to solve this attack within TCE, because it requires
ensuring consistency at the row level, but TCE obviously works at column
level only.

I was under the impression that clients already had to be modified to
figure out how to encrypt the data? If part of that process ends up
including enforcement of encryption for a specific column set, then the
addition of AEAD data could hypothetically be part of that hand-
waviness.

I think "transparency" here means the client just uses the regular
prepared-statement API without having to explicitly encrypt/decrypt any
data. The problem is we can't easily tie this to other columns in the
table, because the client may not even know what values are in those
columns.

The way I originally described my request -- "I'd like to be able to
tie an encrypted value to other column (or external) data" -- was not
very clear.

With my proposed model -- where the DBA (and the server) are completely
untrusted, and the DBA needs to be prevented from using the encrypted
value -- I don't think there's a useful way for the client to use
associated data that comes from the server. The client has to know what
the AD should be beforehand, because otherwise the DBA can make it so
the server returns whatever is correct.

True. With untrusted server the additional data would have to come from
some other source. Say, an isolated auth system or so.

Imagine you do this

UPDATE t SET encrypted_column = $1 WHERE another_column = $2;

but you want to ensure the encrypted value belongs to a particular row
(which may or may not be identified by the another_column value). How
would the client do that? Should it fetch the value or what?

Similarly, what if the client just does

SELECT encrypted_column FROM t;

How would it verify the values belong to the row, without having all the
data for the row (or just the required columns)?

So with my (hopefully more clear) model above, it wouldn't. The client
would already have the AD, and somehow tell libpq what that data was
for the query.

The rabbit hole I led you down is one where we use the rest of the row
as AD, to try to freeze pieces of it in place. That might(?) have some
useful security properties (if the client defines its use and doesn't
defer to the server). But it's not what I intended to propose and I'd
have to think about that case some more.

OK

In my credit card example, I'm imagining something like (forgive the
contrived syntax):

SELECT address, :{aead(users.credit_card, 'user@example.com')}
FROM users WHERE email = 'user@example.com';

UPDATE users
SET :{aead(users.credit_card, 'user@example.com')} = '1234-...'
WHERE email = 'user@example.com';

The client explicitly links a table's column to its AD for the duration
of the query. This approach can't scale to

SELECT credit_card FROM users;

because in this case the AD for each row is different, but I'd argue
that's ideal for this particular case. The client doesn't need to (and
probably shouldn't) grab everyone's credit card details all at once, so
there's no reason to optimize for it.

Maybe, but it seems like a rather annoying limitation, as it restricts
the client to single-row queries (or at least it looks like that to me).
Yes, it may be fine for some use cases, but I'd bet a DBA who can modify
data can do plenty other things - swapping "old" values, which will have
the right AD, for example.

Unless "transparent" means that the client completely defers to the
server on whether to encrypt or not, and silently goes along with it if
the server tells it not to encrypt?

I think that's probably a valid concern - a "bad DBA" could alter the
table definition to not contain the "ENCRYPTED" bits, and then peek at
the plaintext values.

But it's not clear to me how exactly would the AEAD prevent this?
Wouldn't that be also specified on the server, somehow? In which case
the DBA could just tweak that too, no?

In other words, this issue seems mostly orthogonal to the AEAD, and the
right solution would be to allow the client to define which columns have
to be encrypted (in which case altering the server definition would not
be enough).

Right, exactly. When I mentioned AEAD I had assumed that "allow the
client to define which columns have to be encrypted" was already
planned or in the works; I just misunderstood pieces of Peter's email.
It's that piece where a client would probably have to add details
around AEAD and its use.

That would only protect against a
_completely_ passive DBA, like someone reading unencrypted backups,
etc. And that still has a lot of value, certainly. But it seems like
this prototype is very close to a system where the client can reliably
secure data even if the server isn't trustworthy, if that's a use case
you're interested in.

Right. IMHO the "passive attacker" is a perfectly fine model for use
cases that would be fine with e.g. pgcrypto if there was no risk of
leaking plaintext values to logs, system catalogs, etc.

If we can improve it to provide (at least some) protection against
active attackers, that'd be a nice bonus.

I agree that resistance against offline attacks is a useful step
forward (it seems to be a strict improvement over pgcrypto). I have a
feeling that end users will *expect* some protection against online
attacks too, since an evil DBA is going to be well-positioned to do
exactly that.

Yeah.

It's probably possible to get something like this (row-level AEAD) by
encrypting enriched data, i.e. not just the card number, but {user ID,
card number} or something like that, and verify that in the webapp. The
problem of course is that the "user ID" is just another column in the
table, and there's nothing preventing the DBA from modifying that too.

Right. That's why the client has to be able to choose AD according to
the application. In my previous example, the victim's email address can
be copied by the DBA, but they wouldn't be able to authenticate as that
user and couldn't convince the client to use the plaintext on their
behalf.

Well, yeah. But I'm not sure how to make that work easily, because the
client may not have the data :-(

I was thinking about using a composite data type combining the data with
the extra bits - that'd not be all that transparent as it'd require the
client to build this manually and then also cross-check it after loading
the data. So the user would be responsible for having all the data.

But doing that automatically/transparently seems hard, because how would
you deal e.g. with SELECT queries reading data through a view or CTE?

How would you declare this, either at the client or server?

I'll do some more thinking on the case you're talking about here, where
pieces of the row are transparently tied together.

OK. In any case, I think we shouldn't require this capability from the
get go - it's fine to get the simple version done first, which gives us
privacy / protects against passive attacker. And then sometime in the
future improve this further.

Do any other databases have this capability? How do they do it?

BigQuery advertises AEAD support. I don't think their model is the same
as ours, though; from the docs it looks like it's essentially pgcrypto,
where you tell the server to encrypt stuff for you.

Pretty sure it's server-side. The docs say it's for encryption at rest,
all the examples do the encryption/decryption in SQL, etc.

regards

--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#12Greg Stark
stark@mit.edu
In reply to: Peter Eisentraut (#1)
Re: Transparent column encryption

In the server, the encrypted datums are stored in types called
encryptedr and encryptedd (for randomized and deterministic
encryption). These are essentially cousins of bytea.

Does that mean someone could go in with psql and select out the data
without any keys and just get a raw bytea-like representation? That
seems like a natural and useful thing to be able to do. For example to
allow dumping a table and loading it elsewhere and transferring keys
through some other channel (perhaps only as needed).

#13Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Greg Stark (#12)
Re: Transparent column encryption

On 16.12.21 05:47, Greg Stark wrote:

In the server, the encrypted datums are stored in types called
encryptedr and encryptedd (for randomized and deterministic
encryption). These are essentially cousins of bytea.

Does that mean someone could go in with psql and select out the data
without any keys and just get a raw bytea-like representation? That
seems like a natural and useful thing to be able to do. For example to
allow dumping a table and loading it elsewhere and transferring keys
through some other channel (perhaps only as needed).

Yes to all of that.

#14Jacob Champion
pchampion@vmware.com
In reply to: Tomas Vondra (#11)
Re: Transparent column encryption

On Thu, 2021-12-09 at 11:04 +0100, Tomas Vondra wrote:

On 12/9/21 01:12, Jacob Champion wrote:

The rabbit hole I led you down is one where we use the rest of the row
as AD, to try to freeze pieces of it in place. That might(?) have some
useful security properties (if the client defines its use and doesn't
defer to the server). But it's not what I intended to propose and I'd
have to think about that case some more.

So after thinking about it some more, in the case where the client is
relying on the server to return both the encrypted data and its
associated data -- and you don't trust the server -- then tying even
the entire row together doesn't help you.

I was briefly led astray by the idea that you could include a unique or
primary key column in the associated data, and then SELECT based on
that column -- but a motivated DBA could simply corrupt state so that
the row they wanted got returned regardless of the query. So the client
still has to have prior knowledge.

In my credit card example, I'm imagining something like (forgive the
contrived syntax):

SELECT address, :{aead(users.credit_card, 'user@example.com')}
FROM users WHERE email = 'user@example.com';

UPDATE users
SET :{aead(users.credit_card, 'user@example.com')} = '1234-...'
WHERE email = 'user@example.com';

The client explicitly links a table's column to its AD for the duration
of the query. This approach can't scale to

SELECT credit_card FROM users;

because in this case the AD for each row is different, but I'd argue
that's ideal for this particular case. The client doesn't need to (and
probably shouldn't) grab everyone's credit card details all at once, so
there's no reason to optimize for it.

Maybe, but it seems like a rather annoying limitation, as it restricts
the client to single-row queries (or at least it looks like that to me).
Yes, it may be fine for some use cases, but I'd bet a DBA who can modify
data can do plenty other things - swapping "old" values, which will have
the right AD, for example.

Resurrecting old data doesn't help the DBA read the values, right? I
view that as similar to the "increasing account balance" problem, in
that it's definitely a problem but not one we're trying to tackle here.

(And I'm not familiar with any solutions for resurrections -- other
than having data expire and tying the timestamp into the
authentication, which I think again requires AD. Revoking signed data
is one of those hard problems. Do you know a better way?)

OK. In any case, I think we shouldn't require this capability from the
get go - it's fine to get the simple version done first, which gives us
privacy / protects against passive attacker. And then sometime in the
future improve this further.

Agreed. (And I think the client should be able to enforce encryption in
the first place, before I distract you too much with other stuff.)

--Jacob

#15Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#14)
Re: Transparent column encryption

On 17.12.21 01:41, Jacob Champion wrote:

(And I think the client should be able to enforce encryption in
the first place, before I distract you too much with other stuff.)

Yes, this is a useful point that I have added to my notes.

#16Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#1)
1 attachment(s)
Re: Transparent column encryption

Here is a new version of this patch. See also the original description
quoted below. I have done a significant amount of work on this over the
last few months. Some important news include:

- The cryptography has been improved. It now uses an AEAD scheme, and
for deterministic encryption a proper SIV construction.

- The OpenSSL-specific parts have been moved to a separate file in
libpq. Non-OpenSSL builds compile and work (without functionality, of
course).

- libpq handles multiple CEKs and CMKs, including changing keys on the fly.

- libpq supports a mode to force encryption of certain values.

- libpq supports a flexible configuration system for looking up CMKs,
including support for external key management systems.

- psql has a new \gencr command that allows passing in bind parameters
for (potential) encryption.

- There is some more pg_dump and psql support.

- The new data types for storing encrypted data have been renamed for
clarity.

- Various changes to the protocol compared to the previous patch.

- The patch contains full documentation of the protocol changes,
glossary entries, and more new documentation.

The major pieces that are still missing are:

- DDL support for registering keys

- Protocol versioning or feature flags

Other than that it's pretty complete in my mind.

For interested reviewers, I have organized the patch so that you can
start reading it top to bottom: The documentation comes first, then the
tests, then the code changes. Even some feedback on the first or first
two aspects would be valuable to me.

Old news follows:

Show quoted text

On 03.12.21 22:32, Peter Eisentraut wrote:

I want to present my proof-of-concept patch for the transparent column
encryption feature.  (Some might also think of it as automatic
client-side encryption or similar, but I like my name.)  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
the "prying eyes" of DBAs, sysadmins, cloud operators, 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.  Of course, you 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 you can do equality lookups, at the cost of some
security.

This functionality also exists in other SQL database products, so the
overall concepts weren't invented by me by any means.

Also, this feature has nothing to do with the on-disk encryption
feature being contemplated in parallel.  Both can exist independently.

The attached patch 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.  But it's missing the remaining 90% of the work,
including additional DDL support, error handling, robust memory
management, protocol versioning, forward and backward compatibility,
pg_dump support, psql \d support, refinement of the cryptography, and
so on.  But I think obvious solutions exist to all of those things, so
it isn't that interesting to focus on them for now.

------

Now to the explanation of how it works.

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, the catalog object specifies a "provider" and generic
options.  Right now, libpq has a "file" provider hardcoded, and it
takes a "filename" option.  Via some mechanism to be determined,
additional providers could be loaded and then talk to key management
systems via http or whatever.  I have left some comments in the libpq
code where the hook points for this could be.

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.)

The encryption algorithms are mostly hardcoded right now, but there
are facilities for picking algorithms and adding new ones that will be
expanded.  The CMK process uses RSA-OAEP.  The CEK process uses
AES-128-CBC right now; a more complete solution should probably
involve some HMAC thrown in.

In the server, the encrypted datums are stored in types called
encryptedr and encryptedd (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, encryptedr has no operators at all,
encryptedd 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.

Some protocol extensions are required.  These should be guarded by
some _pq_... setting, but this is not done in this patch yet.  As
mentioned above, extra messages are added for sending the CMKs and
CEKs.  In the RowDescription message, I have commandeered the format
field to add a bit that indicates that the field is encrypted.  This
could be made a separate field, and there should probably be
additional fields to indicate the algorithm and CEK name, but this was
easiest for now.  The ParameterDescription message is extended to
contain format fields for each parameter, for the same purpose.
Again, this could be done differently.

Speaking of parameter descriptions, 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.

And also, psql doesn't use prepared statements, so writing into
encrypted columns currently doesn't work at all via psql.  (Reading
works no problem.)  All the test code currently uses custom libpq C
programs.  We should think about a way to enable prepared statements
in psql, perhaps something like

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

(\gexec and \gx are already taken.)

------

This is not targeting PostgreSQL 15.  But I'd appreciate some feedback
on the direction.  As I mentioned above, a lot of the remaining work
is arguably mostly straightforward.  Some closer examination of the
issues surrounding the libpq API changes and psql would be useful.
Perhaps there are other projects where that kind of functionality
would also be useful.

Attachments:

v2-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v2-0001-Transparent-column-encryption.patchDownload
From 9ab9a26b865717aaa7575afe7e771bdb63da12eb Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 29 Jun 2022 00:01:22 +0100
Subject: [PATCH v2] Transparent column encryption

---
 doc/src/sgml/acronyms.sgml                    |  18 +
 doc/src/sgml/catalogs.sgml                    | 275 +++++++
 doc/src/sgml/datatype.sgml                    |  47 ++
 doc/src/sgml/ddl.sgml                         |  73 ++
 doc/src/sgml/glossary.sgml                    |  23 +
 doc/src/sgml/libpq.sgml                       | 257 +++++-
 doc/src/sgml/protocol.sgml                    | 346 ++++++++
 doc/src/sgml/ref/create_table.sgml            |  42 +-
 doc/src/sgml/ref/psql-ref.sgml                |  33 +
 src/test/Makefile                             |   3 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  29 +
 .../t/001_column_encryption.pl                | 206 +++++
 .../column_encryption/t/002_cmk_rotation.pl   | 144 ++++
 src/test/column_encryption/test_client.c      | 210 +++++
 .../column_encryption/test_run_decrypt.pl     |  41 +
 .../traces/disallowed_in_pipeline.trace       |   2 +-
 .../traces/multi_pipelines.trace              |   4 +-
 .../libpq_pipeline/traces/nosync.trace        |  20 +-
 .../traces/pipeline_abort.trace               |   4 +-
 .../libpq_pipeline/traces/prepared.trace      |   6 +-
 .../traces/simple_pipeline.trace              |   2 +-
 .../libpq_pipeline/traces/singlerow.trace     |   6 +-
 .../libpq_pipeline/traces/transaction.trace   |   2 +-
 .../regress/expected/column_encryption.out    |  59 ++
 src/test/regress/expected/oidjoins.out        |   6 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/prepare.out         |  41 +-
 src/test/regress/expected/psql.out            |  25 +
 src/test/regress/expected/rules.out           |   6 +-
 src/test/regress/expected/type_sanity.out     |  20 +-
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    |  50 ++
 src/test/regress/sql/prepare.sql              |   6 +-
 src/test/regress/sql/psql.sql                 |  17 +
 src/test/regress/sql/type_sanity.sql          |   4 +-
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |   9 +
 src/include/catalog/pg_cast.dat               |   6 +
 src/include/catalog/pg_colenckey.h            |  55 ++
 src/include/catalog/pg_colenckeydata.h        |  46 ++
 src/include/catalog/pg_colmasterkey.h         |  45 ++
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  10 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               |  13 +-
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/analyze.h                  |   4 +-
 src/include/parser/parse_node.h               |   2 +
 src/include/parser/parse_param.h              |   3 +-
 src/include/tcop/tcopprot.h                   |   2 +
 src/include/utils/plancache.h                 |   4 +
 src/include/utils/syscache.h                  |   3 +
 src/backend/access/common/printsimple.c       |   2 +
 src/backend/access/common/printtup.c          | 137 ++++
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/heap.c                    |   3 +
 src/backend/commands/prepare.c                |  59 +-
 src/backend/commands/tablecmds.c              |  74 ++
 src/backend/executor/spi.c                    |   4 +
 src/backend/nodes/copyfuncs.c                 |   1 +
 src/backend/nodes/equalfuncs.c                |   1 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/nodes/outfuncs.c                  |   1 +
 src/backend/parser/analyze.c                  |   3 +-
 src/backend/parser/gram.y                     |  13 +-
 src/backend/parser/parse_param.c              |  43 +-
 src/backend/parser/parse_target.c             |   6 +
 src/backend/replication/basebackup_copy.c     |  12 +
 src/backend/replication/walsender.c           |   5 +
 src/backend/tcop/postgres.c                   |  54 +-
 src/backend/utils/cache/plancache.c           |  10 +
 src/backend/utils/cache/syscache.c            |  29 +
 src/bin/pg_dump/pg_dump.c                     |  26 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/command.c                        |  31 +
 src/bin/psql/common.c                         |  38 +-
 src/bin/psql/describe.c                       |  29 +-
 src/bin/psql/help.c                           |   3 +
 src/bin/psql/settings.h                       |   5 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |   2 +-
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  20 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 753 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 594 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 123 ++-
 src/interfaces/libpq/fe-trace.c               |   7 +
 src/interfaces/libpq/libpq-fe.h               |  22 +
 src/interfaces/libpq/libpq-int.h              |  34 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 .../postgres_fdw/expected/postgres_fdw.out    |   2 +-
 103 files changed, 4380 insertions(+), 123 deletions(-)
 create mode 100644 src/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/t/002_cmk_rotation.pl
 create mode 100644 src/test/column_encryption/test_client.c
 create mode 100755 src/test/column_encryption/test_run_decrypt.pl
 create mode 100644 src/test/regress/expected/column_encryption.out
 create mode 100644 src/test/regress/sql/column_encryption.sql
 create mode 100644 src/include/catalog/pg_colenckey.h
 create mode 100644 src/include/catalog/pg_colenckeydata.h
 create mode 100644 src/include/catalog/pg_colmasterkey.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h

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

base-commit: c1d033fcb5ecf306241cd729d1edcaa846456335
-- 
2.36.1

#17Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#16)
1 attachment(s)
Re: Transparent column encryption

Rebased patch, no new functionality.

Show quoted text

On 29.06.22 01:29, Peter Eisentraut wrote:

Here is a new version of this patch.  See also the original description
quoted below.  I have done a significant amount of work on this over the
last few months.  Some important news include:

- The cryptography has been improved.  It now uses an AEAD scheme, and
for deterministic encryption a proper SIV construction.

- The OpenSSL-specific parts have been moved to a separate file in
libpq.  Non-OpenSSL builds compile and work (without functionality, of
course).

- libpq handles multiple CEKs and CMKs, including changing keys on the fly.

- libpq supports a mode to force encryption of certain values.

- libpq supports a flexible configuration system for looking up CMKs,
including support for external key management systems.

- psql has a new \gencr command that allows passing in bind parameters
for (potential) encryption.

- There is some more pg_dump and psql support.

- The new data types for storing encrypted data have been renamed for
clarity.

- Various changes to the protocol compared to the previous patch.

- The patch contains full documentation of the protocol changes,
glossary entries, and more new documentation.

The major pieces that are still missing are:

- DDL support for registering keys

- Protocol versioning or feature flags

Other than that it's pretty complete in my mind.

For interested reviewers, I have organized the patch so that you can
start reading it top to bottom: The documentation comes first, then the
tests, then the code changes.  Even some feedback on the first or first
two aspects would be valuable to me.

Old news follows:

On 03.12.21 22:32, Peter Eisentraut wrote:

I want to present my proof-of-concept patch for the transparent column
encryption feature.  (Some might also think of it as automatic
client-side encryption or similar, but I like my name.)  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
the "prying eyes" of DBAs, sysadmins, cloud operators, 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.  Of course, you 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 you can do equality lookups, at the cost of some
security.

This functionality also exists in other SQL database products, so the
overall concepts weren't invented by me by any means.

Also, this feature has nothing to do with the on-disk encryption
feature being contemplated in parallel.  Both can exist independently.

The attached patch 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.  But it's missing the remaining 90% of the work,
including additional DDL support, error handling, robust memory
management, protocol versioning, forward and backward compatibility,
pg_dump support, psql \d support, refinement of the cryptography, and
so on.  But I think obvious solutions exist to all of those things, so
it isn't that interesting to focus on them for now.

------

Now to the explanation of how it works.

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, the catalog object specifies a "provider" and generic
options.  Right now, libpq has a "file" provider hardcoded, and it
takes a "filename" option.  Via some mechanism to be determined,
additional providers could be loaded and then talk to key management
systems via http or whatever.  I have left some comments in the libpq
code where the hook points for this could be.

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.)

The encryption algorithms are mostly hardcoded right now, but there
are facilities for picking algorithms and adding new ones that will be
expanded.  The CMK process uses RSA-OAEP.  The CEK process uses
AES-128-CBC right now; a more complete solution should probably
involve some HMAC thrown in.

In the server, the encrypted datums are stored in types called
encryptedr and encryptedd (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, encryptedr has no operators at all,
encryptedd 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.

Some protocol extensions are required.  These should be guarded by
some _pq_... setting, but this is not done in this patch yet.  As
mentioned above, extra messages are added for sending the CMKs and
CEKs.  In the RowDescription message, I have commandeered the format
field to add a bit that indicates that the field is encrypted.  This
could be made a separate field, and there should probably be
additional fields to indicate the algorithm and CEK name, but this was
easiest for now.  The ParameterDescription message is extended to
contain format fields for each parameter, for the same purpose.
Again, this could be done differently.

Speaking of parameter descriptions, 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.

And also, psql doesn't use prepared statements, so writing into
encrypted columns currently doesn't work at all via psql.  (Reading
works no problem.)  All the test code currently uses custom libpq C
programs.  We should think about a way to enable prepared statements
in psql, perhaps something like

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

(\gexec and \gx are already taken.)

------

This is not targeting PostgreSQL 15.  But I'd appreciate some feedback
on the direction.  As I mentioned above, a lot of the remaining work
is arguably mostly straightforward.  Some closer examination of the
issues surrounding the libpq API changes and psql would be useful.
Perhaps there are other projects where that kind of functionality
would also be useful.

Attachments:

v3-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v3-0001-Transparent-column-encryption.patchDownload
From bcb9df8eaafdd1b5a05d8ea4bb5b2af001f6403f Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Tue, 5 Jul 2022 12:48:18 +0200
Subject: [PATCH v3] Transparent column encryption

---
 doc/src/sgml/acronyms.sgml                    |  18 +
 doc/src/sgml/catalogs.sgml                    | 275 +++++++
 doc/src/sgml/datatype.sgml                    |  47 ++
 doc/src/sgml/ddl.sgml                         |  73 ++
 doc/src/sgml/glossary.sgml                    |  23 +
 doc/src/sgml/libpq.sgml                       | 257 +++++-
 doc/src/sgml/protocol.sgml                    | 346 ++++++++
 doc/src/sgml/ref/create_table.sgml            |  42 +-
 doc/src/sgml/ref/psql-ref.sgml                |  33 +
 src/test/Makefile                             |   3 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  29 +
 .../t/001_column_encryption.pl                | 206 +++++
 .../column_encryption/t/002_cmk_rotation.pl   | 144 ++++
 src/test/column_encryption/test_client.c      | 210 +++++
 .../column_encryption/test_run_decrypt.pl     |  41 +
 .../traces/disallowed_in_pipeline.trace       |   2 +-
 .../traces/multi_pipelines.trace              |   4 +-
 .../libpq_pipeline/traces/nosync.trace        |  20 +-
 .../traces/pipeline_abort.trace               |   4 +-
 .../libpq_pipeline/traces/prepared.trace      |   6 +-
 .../traces/simple_pipeline.trace              |   2 +-
 .../libpq_pipeline/traces/singlerow.trace     |   6 +-
 .../libpq_pipeline/traces/transaction.trace   |   2 +-
 .../regress/expected/column_encryption.out    |  59 ++
 src/test/regress/expected/oidjoins.out        |   6 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/prepare.out         |  38 +-
 src/test/regress/expected/psql.out            |  25 +
 src/test/regress/expected/rules.out           |   4 +-
 src/test/regress/expected/type_sanity.out     |  20 +-
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    |  50 ++
 src/test/regress/sql/prepare.sql              |   2 +-
 src/test/regress/sql/psql.sql                 |  17 +
 src/test/regress/sql/type_sanity.sql          |   4 +-
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |   9 +
 src/include/catalog/pg_cast.dat               |   6 +
 src/include/catalog/pg_colenckey.h            |  55 ++
 src/include/catalog/pg_colenckeydata.h        |  46 ++
 src/include/catalog/pg_colmasterkey.h         |  45 ++
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  10 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               |  13 +-
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/nodes/parsenodes.h                |   1 +
 src/include/parser/analyze.h                  |   4 +-
 src/include/parser/parse_node.h               |   2 +
 src/include/parser/parse_param.h              |   3 +-
 src/include/tcop/tcopprot.h                   |   2 +
 src/include/utils/plancache.h                 |   6 +
 src/include/utils/syscache.h                  |   4 +
 src/backend/access/common/printsimple.c       |   2 +
 src/backend/access/common/printtup.c          | 162 ++++
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/heap.c                    |   3 +
 src/backend/commands/prepare.c                |  74 +-
 src/backend/commands/tablecmds.c              |  74 ++
 src/backend/executor/spi.c                    |   4 +
 src/backend/nodes/copyfuncs.c                 |   1 +
 src/backend/nodes/equalfuncs.c                |   1 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/nodes/outfuncs.c                  |   1 +
 src/backend/parser/analyze.c                  |   3 +-
 src/backend/parser/gram.y                     |  13 +-
 src/backend/parser/parse_param.c              |  43 +-
 src/backend/parser/parse_target.c             |   6 +
 src/backend/tcop/postgres.c                   |  54 +-
 src/backend/utils/adt/arrayfuncs.c            |   1 +
 src/backend/utils/cache/plancache.c           |  10 +
 src/backend/utils/cache/syscache.c            |  39 +
 src/bin/pg_dump/pg_dump.c                     |  26 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/command.c                        |  31 +
 src/bin/psql/common.c                         |  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                   |   2 +-
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  20 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 758 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 594 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 123 ++-
 src/interfaces/libpq/fe-trace.c               |   7 +
 src/interfaces/libpq/libpq-fe.h               |  22 +
 src/interfaces/libpq/libpq-int.h              |  34 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 .../postgres_fdw/expected/postgres_fdw.out    |   2 +-
 102 files changed, 4407 insertions(+), 130 deletions(-)
 create mode 100644 src/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/t/002_cmk_rotation.pl
 create mode 100644 src/test/column_encryption/test_client.c
 create mode 100755 src/test/column_encryption/test_run_decrypt.pl
 create mode 100644 src/test/regress/expected/column_encryption.out
 create mode 100644 src/test/regress/sql/column_encryption.sql
 create mode 100644 src/include/catalog/pg_colenckey.h
 create mode 100644 src/include/catalog/pg_colenckeydata.h
 create mode 100644 src/include/catalog/pg_colmasterkey.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h

diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 9ed148ab84..e21c9bcce3 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -56,6 +56,15 @@ <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 4f3f375a84..c5e30f8029 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>
@@ -2437,6 +2486,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 8e30b82273..cdd0530eeb 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5342,4 +5342,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 b01e3ad544..bb8503584e 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1211,6 +1211,79 @@ <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>
+
+  <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>
+ </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 d6d0a3a814..10507e4391 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 74456aa69d..310aa49e37 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1974,6 +1974,110 @@ <title>Parameter Key Words</title>
        </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>
@@ -3022,6 +3126,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>
 
@@ -3872,6 +4027,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>
 
@@ -4047,12 +4224,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>
 
@@ -4578,6 +4780,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
@@ -4585,6 +4788,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.
@@ -4696,6 +4900,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>
 
@@ -4746,6 +4990,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"/>
@@ -7776,6 +8021,16 @@ <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>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index c0b89a3c01..60595c33b1 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1050,6 +1050,52 @@ <title>Extended Query</title>
    </note>
   </sect2>
 
+  <sect2>
+   <title>Transparent Column Encryption</title>
+
+   <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>
+    The cryptographic operations used for transparent column encryption are
+    described in <xref linkend="protocol-transparent-column-encryption"/>.
+   </para>
+  </sect2>
+
   <sect2>
    <title>Function Call</title>
 
@@ -3991,6 +4037,128 @@ <title>Message Formats</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ColumnEncryptionKey">
+    <term>ColumnEncryptionKey (B)</term>
+    <listitem>
+     <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>
+     <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>
@@ -5086,6 +5254,37 @@ <title>Message Formats</title>
         </para>
        </listitem>
       </varlistentry>
+
+      <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>
@@ -5474,6 +5673,26 @@ <title>Message Formats</title>
         </para>
        </listitem>
       </varlistentry>
+
+      <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 encrypt algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
      </variablelist>
     </listitem>
    </varlistentry>
@@ -7278,6 +7497,133 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-transparent-column-encryption">
+  <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/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 6c9918b0a1..7a1d03f868 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -22,7 +22,7 @@
  <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> [ 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> [ COMPRESSION <replaceable>compression_method</replaceable> ] [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -322,6 +322,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/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 65bb0a6a3f..e6c93f1b13 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2255,6 +2255,28 @@ <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>
 
@@ -3945,6 +3967,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/src/test/Makefile b/src/test/Makefile
index 69ef074d75..2300be8332 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -32,6 +32,7 @@ SUBDIRS += ldap
 endif
 endif
 ifeq ($(with_ssl),openssl)
+SUBDIRS += column_encryption
 ifneq (,$(filter ssl,$(PG_TEST_EXTRA)))
 SUBDIRS += ssl
 endif
@@ -41,7 +42,7 @@ endif
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl column_encryption)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..7aca84b17a
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,29 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/column_encryption/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..7ab98981d8
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,206 @@
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+
+	system_or_bail 'openssl', 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+
+	$node->safe_psql('postgres', qq{
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkowner, cmkrealm) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    '${cmkname}',
+    (select oid from pg_roles where rolname = current_user),
+   ''
+);
+});
+
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	# generate random bytes
+	system_or_bail 'openssl', 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	system_or_bail 'openssl', 'pkeyutl', '-encrypt',
+	  '-inkey', $cmkfilename,
+	  '-pkeyopt', 'rsa_padding_mode:oaep',
+	  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+	  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+	my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+	# create CEK in database
+	$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckey (oid, cekname, cekowner) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    '${cekname}',
+    (select oid from pg_roles where rolname = current_user)
+);
+});
+	$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = '${cekname}'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = '${cmkname}'),
+    1,
+    '\\x${cekenchex}'
+);
+});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 32, 'cmk1', $cmk1filename);
+create_cek('cek2', 48, 'cmk2', $cmk2filename);
+
+$ENV{'PGCMKLOOKUP'} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl3 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c text ENCRYPTED WITH (column_encryption_key = cek2, algorithm = 'AEAD_AES_192_CBC_HMAC_SHA_384')
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b) VALUES (1, $1) \gencr 'val1'
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \gencr 'val2'
+});
+
+# Expected ciphertext length is 2 blocks of AES output (2 * 16) plus
+# half SHA-256 output (16) in hex encoding: (2 * 16 + 16) * 2 = 96
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1) TO STDOUT}),
+	qr/1\t\\\\x[0-9a-f]{96}\n2\t\\\\x[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'decrypted query result');
+
+{
+	local $ENV{'PGCMKLOOKUP'} = '*=run:broken %k "%b"';
+	$result = $node->psql('postgres', q{SELECT a, b FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{'PGCMKLOOKUP'} = "*=run:perl ./test_run_decrypt.pl '${PostgreSQL::Test::Utils::tmp_check}' %k %a '%b'";
+
+	$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+	is($result,
+		q(1|val1
+2|val2),
+		'decrypted query result with cmklookup run');
+}
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES (1, $1, $2) \gencr 'valB1' 'valC1'
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+
+$node->command_fails_like(['test_client', 'test1'], qr/not encrypted/, 'test client fails because parameters not encrypted');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'decrypted query result after test client insert');
+
+$node->command_ok(['test_client', 'test2'], 'test client test 2');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3),
+	'decrypted query result after test client insert 2');
+
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1 WHERE a = 3) TO STDOUT}),
+	qr/3\t\\\\x[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+$node->command_ok(['test_client', 'test3'], 'test client test 3');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3upd),
+	'decrypted query result after test client insert 3');
+
+$node->command_ok(['test_client', 'test4'], 'test client test 4');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result after test client insert 4');
+
+is($node->safe_psql('postgres', q{SELECT b, count(*) FROM tbl2 GROUP BY b ORDER BY 2}),
+	q(valB|1
+valA|2),
+	'group by deterministically encrypted column');
+
+$node->command_ok(['test_client', 'test5'], 'test client test 5');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1
+2|valB2|valC2
+3|valB3|valC3),
+	'decrypted query result multiple keys after test client insert 5');
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..5a13a206eb
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,144 @@
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Test column master key rotation.  First, we generate CMK1 and a CEK
+# encrypted with it.  Then we add a CMK2 and encrypt the CEK with it
+# as well.  (Recall that a CEK can be associated with multiple CMKs,
+# for this reason.  That's why pg_colenckeydata is split out from
+# pg_colenckey.)  Then we remove CMK1.  We test that we can get
+# decrypted query results at each step.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init;
+$node->start;
+
+
+sub create_cmk
+{
+	my ($cmkname) = @_;
+
+	my $cmkfilename = "${PostgreSQL::Test::Utils::tmp_check}/${cmkname}.pem";
+
+	system_or_bail 'openssl', 'genpkey', '-algorithm', 'rsa', '-out', $cmkfilename;
+
+	$node->safe_psql('postgres', qq{
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkowner, cmkrealm) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    '${cmkname}',
+    (select oid from pg_roles where rolname = current_user),
+   ''
+);
+});
+
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 16+16);
+
+# generate random bytes
+system_or_bail 'openssl', 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+# encrypt CEK using CMK
+system_or_bail 'openssl', 'pkeyutl', '-encrypt',
+  '-inkey', $cmk1filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+my $cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# create CEK in database
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckey (oid, cekname, cekowner) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    '${cekname}',
+    (select oid from pg_roles where rolname = current_user)
+);
+});
+
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = '${cekname}'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    1,
+    '\\x${cekenchex}'
+);
+});
+
+$ENV{'PGCMKLOOKUP'} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b) VALUES (1, $1) \gencr 'val1'
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \gencr 'val2'
+});
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with one CMK');
+
+
+# create new CMK
+my $cmk2filename = create_cmk('cmk2');
+
+# encrypt CEK using new CMK
+#
+# (Here, we still have the plaintext of the CEK available from
+# earlier.  In reality, one would decrypt the CEK with the first CMK
+# and then re-encrypt it with the second CMK.)
+system_or_bail 'openssl', 'pkeyutl', '-encrypt',
+  '-inkey', $cmk2filename,
+  '-pkeyopt', 'rsa_padding_mode:oaep',
+  '-in', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin",
+  '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc";
+
+$cekenchex = unpack('H*', slurp_file "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin.enc");
+
+# add new data record for CEK in database
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = '${cekname}'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk2'),
+    1,
+    '\\x${cekenchex}'
+);
+});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with two CMKs');
+
+
+# delete CEK record for first CMK
+$node->safe_psql('postgres', qq{
+DELETE FROM pg_colenckeydata WHERE ckdcmkid = (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1');
+});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with only new CMK');
+
+
+done_testing();
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..add9fe40a6
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2021-2022, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3"};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					   2, NULL, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3"};
+	int			forces[] = {false, true};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					2, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (!(!PQparamisencrypted(res2, 0) &&
+		  PQparamisencrypted(res2, 1)))
+	{
+		fprintf(stderr, "wrong results from PQparamisencrypted()\n");
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, forces, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3upd"};
+
+	res = PQprepare(conn, "", "UPDATE tbl1 SET b = $2 WHERE a = $1",
+					2, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"1", "valA", "2", "valB", "3", "valA"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6)",
+					6, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 6, values, NULL, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test5(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {
+		"2", "valB2", "valC2",
+		"3", "valB3", "valC3"
+	};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6)",
+					6, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 6, values, NULL, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	PGconn	   *conn;
+	int			ret = 0;
+
+	conn = PQconnectdb("");
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		fprintf(stderr, "Connection to database failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (argc < 2 || argv[1] == NULL)
+		return 87;
+	else if (strcmp(argv[1], "test1") == 0)
+		ret = test1(conn);
+	else if (strcmp(argv[1], "test2") == 0)
+		ret = test2(conn);
+	else if (strcmp(argv[1], "test3") == 0)
+		ret = test3(conn);
+	else if (strcmp(argv[1], "test4") == 0)
+		ret = test4(conn);
+	else if (strcmp(argv[1], "test5") == 0)
+		ret = test5(conn);
+	else
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..9664349f14
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,41 @@
+#!/usr/bin/perl
+
+# Test/sample command for libpq cmklookup run scheme
+#
+# This just places the data into temporary files and runs the openssl
+# command on it.  (In practice, this could more simply be written as a
+# shell script, but this way it's more portable.)
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use MIME::Base64;
+
+my ($tmpdir, $cmkname, $alg, $b64data) = @ARGV;
+
+die unless $alg eq 'RSAES_OAEP_SHA_1';
+
+open my $fh, '>:raw', "${tmpdir}/input.tmp" or die;
+print $fh decode_base64($b64data);
+close $fh;
+
+system('openssl', 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', "${tmpdir}/input.tmp", '-out', "${tmpdir}/output.tmp") == 0 or die;
+
+open $fh, '<:raw', "${tmpdir}/output.tmp" or die;
+my $data = '';
+
+while (1) {
+	my $success = read $fh, $data, 100, length($data);
+	die $! if not defined $success;
+	last if not $success;
+}
+
+close $fh;
+
+unlink "${tmpdir}/input.tmp", "${tmpdir}/output.tmp";
+
+print encode_base64($data);
diff --git a/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace b/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace
index dd6df03f1e..5fc9c0346d 100644
--- a/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace
+++ b/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace
@@ -1,5 +1,5 @@
 F	13	Query	 "SELECT 1"
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace b/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace
index 4b9ab07ca4..700b0b6519 100644
--- a/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace
+++ b/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace
@@ -10,13 +10,13 @@ F	9	Execute	 "" 0
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/nosync.trace b/src/test/modules/libpq_pipeline/traces/nosync.trace
index d99aac649d..7b1dfa1c15 100644
--- a/src/test/modules/libpq_pipeline/traces/nosync.trace
+++ b/src/test/modules/libpq_pipeline/traces/nosync.trace
@@ -41,52 +41,52 @@ F	9	Execute	 "" 0
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 F	4	Terminate
diff --git a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
index 3fce548b99..6e4309cadd 100644
--- a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
+++ b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
@@ -50,14 +50,14 @@ F	6	Close	 P ""
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 65535 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	32	DataRow	 1 22 '0.33333333333333333333'
 B	32	DataRow	 1 22 '0.50000000000000000000'
 B	32	DataRow	 1 22 '1.00000000000000000000'
 B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "22012" M "division by zero" F "SSSS" L "SSSS" R "SSSS" \x00
 B	5	ReadyForQuery	 I
 F	40	Query	 "SELECT itemno FROM pq_pipeline_demo"
-B	31	RowDescription	 1 "itemno" NNNN 2 NNNN 4 -1 0
+B	37	RowDescription	 1 "itemno" NNNN 2 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '3'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace
index 1a7de5c3e6..8d45bbc0ce 100644
--- a/src/test/modules/libpq_pipeline/traces/prepared.trace
+++ b/src/test/modules/libpq_pipeline/traces/prepared.trace
@@ -2,8 +2,8 @@ F	68	Parse	 "select_one" "SELECT $1, '42', $1::numeric, interval '1 sec'" 1 NNNN
 F	16	Describe	 S "select_one"
 F	4	Sync
 B	4	ParseComplete
-B	10	ParameterDescription	 1 NNNN
-B	113	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 65535 -1 0 "numeric" NNNN 0 NNNN 65535 -1 0 "interval" NNNN 0 NNNN 16 -1 0
+B	18	ParameterDescription	 1 NNNN NNNN 0 0
+B	137	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0 "?column?" NNNN 0 NNNN 65535 -1 0 NNNN 0 "numeric" NNNN 0 NNNN 65535 -1 0 NNNN 0 "interval" NNNN 0 NNNN 16 -1 0 NNNN 0
 B	5	ReadyForQuery	 I
 F	10	Query	 "BEGIN"
 B	10	CommandComplete	 "BEGIN"
@@ -13,6 +13,6 @@ B	19	CommandComplete	 "DECLARE CURSOR"
 B	5	ReadyForQuery	 T
 F	16	Describe	 P "cursor_one"
 F	4	Sync
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	5	ReadyForQuery	 T
 F	4	Terminate
diff --git a/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace b/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace
index 5c94749bc1..5f4849425d 100644
--- a/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace
+++ b/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace
@@ -5,7 +5,7 @@ F	9	Execute	 "" 0
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/singlerow.trace b/src/test/modules/libpq_pipeline/traces/singlerow.trace
index 9de99befcc..199df4d4f4 100644
--- a/src/test/modules/libpq_pipeline/traces/singlerow.trace
+++ b/src/test/modules/libpq_pipeline/traces/singlerow.trace
@@ -13,14 +13,14 @@ F	9	Execute	 "" 0
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	40	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0
+B	46	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	12	DataRow	 1 2 '42'
 B	12	DataRow	 1 2 '43'
 B	12	DataRow	 1 2 '44'
 B	13	CommandComplete	 "SELECT 3"
 B	4	ParseComplete
 B	4	BindComplete
-B	40	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0
+B	46	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	12	DataRow	 1 2 '42'
 B	12	DataRow	 1 2 '43'
 B	12	DataRow	 1 2 '44'
@@ -28,7 +28,7 @@ B	12	DataRow	 1 2 '45'
 B	13	CommandComplete	 "SELECT 4"
 B	4	ParseComplete
 B	4	BindComplete
-B	40	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0
+B	46	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	12	DataRow	 1 2 '42'
 B	12	DataRow	 1 2 '43'
 B	12	DataRow	 1 2 '44'
diff --git a/src/test/modules/libpq_pipeline/traces/transaction.trace b/src/test/modules/libpq_pipeline/traces/transaction.trace
index 1dcc2373c0..a6869c4a5b 100644
--- a/src/test/modules/libpq_pipeline/traces/transaction.trace
+++ b/src/test/modules/libpq_pipeline/traces/transaction.trace
@@ -54,7 +54,7 @@ B	15	CommandComplete	 "INSERT 0 1"
 B	5	ReadyForQuery	 I
 B	5	ReadyForQuery	 I
 F	34	Query	 "SELECT * FROM pq_pipeline_tst"
-B	27	RowDescription	 1 "id" NNNN 1 NNNN 4 -1 0
+B	33	RowDescription	 1 "id" NNNN 1 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '3'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..f8db4e17ff
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,59 @@
+\set HIDE_COLUMN_ENCRYPTION false
+/* Imagine:
+CREATE COLUMN MASTER KEY cmk1 (
+    realm = 'test'
+);
+*/
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkowner, cmkrealm) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    'cmk1',
+    (select oid from pg_roles where rolname = current_user),
+    'test'
+);
+/* Imagine:
+CREATE COLUMN ENCRYPTION KEY cek1 (
+    column_master_key = cmk1,
+    algorithm = '...',
+    encrypted_value = '...'
+);
+*/
+INSERT INTO pg_colenckey (oid, cekname, cekowner) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    'cek1',
+    (select oid from pg_roles where rolname = current_user)
+);
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    1,
+    '\xDEADBEEF'
+);
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+ERROR:  column encryption key "notexist" does not exist
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+\d tbl_29f3
+              Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | text    |           |          | 
+ c      | text    |           |          | 
+
+\d+ tbl_29f3
+                                        Table "public.tbl_29f3"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended | cek1       |              | 
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..2aa0e16323 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attrealtypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,7 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 86d755aa44..4a366074da 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -175,13 +175,14 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+(7 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +198,13 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
  anyrange                    | anymultirange
-(5 rows)
+(6 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -841,6 +843,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+pg_encrypted_det_eq(pg_encrypted_det,pg_encrypted_det)
+pg_encrypted_det_ne(pg_encrypted_det,pg_encrypted_det)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
@@ -988,7 +992,9 @@ WHERE c.castmethod = 'b' AND
  xml               | text              |        0 | a
  xml               | character varying |        0 | a
  xml               | character         |        0 | a
-(10 rows)
+ bytea             | pg_encrypted_det  |        0 | e
+ bytea             | pg_encrypted_rnd  |        0 | e
+(12 rows)
 
 -- **************** pg_conversion ****************
 -- Look for illegal values in pg_conversion fields.
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39..4482a65d24 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 60acbd1241..33956f6993 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -237,6 +237,31 @@ SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g
  3 | Hello    | 4 | t
 (1 row)
 
+-- \gencr
+-- (This just tests the parameter passing; there is no encryption here.)
+CREATE TABLE test_gencr (a int, b text);
+INSERT INTO test_gencr VALUES (1, 'one') \gencr
+SELECT * FROM test_gencr WHERE a = 1 \gencr
+ a |  b  
+---+-----
+ 1 | one
+(1 row)
+
+INSERT INTO test_gencr VALUES ($1, $2) \gencr 2 'two'
+SELECT * FROM test_gencr WHERE a IN ($1, $2) \gencr 2 3
+ a |  b  
+---+-----
+ 2 | two
+(1 row)
+
+-- test parse error
+SELECT * FROM test_gencr WHERE a = x \gencr
+ERROR:  column "x" does not exist
+LINE 1: SELECT * FROM test_gencr WHERE a = x 
+                                           ^
+-- test bind error
+SELECT * FROM test_gencr WHERE a = $1 \gencr
+ERROR:  bind message supplies 0 parameters, but prepared statement "" requires 1
 -- \gexec
 create temporary table gexec_test(a int, b text, c date, d float);
 select format('create index on gexec_test(%I)', attname)
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 7ec3d2688f..42e38d66d1 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 d3ac08c9ee..fc0c5896e4 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -17,7 +17,7 @@ SELECT t1.oid, t1.typname
 FROM pg_type as t1
 WHERE t1.typnamespace = 0 OR
     (t1.typlen <= 0 AND t1.typlen != -1 AND t1.typlen != -2) OR
-    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r')) OR
+    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT t1.typisdefined OR
     (t1.typalign not in ('c', 's', 'i', 'd')) OR
     (t1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -75,7 +75,9 @@ ORDER BY t1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | pg_encrypted_det
+ 8244 | pg_encrypted_rnd
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT t1.oid, t1.typname as basetype, t2.typname as arraytype,
@@ -210,7 +212,8 @@ ORDER BY 1;
  e       | enum_in
  m       | multirange_in
  r       | range_in
-(5 rows)
+ y       | byteain
+(6 rows)
 
 -- Check for bogus typoutput routines
 -- As of 8.0, this check finds refcursor, which is borrowing
@@ -255,7 +258,8 @@ ORDER BY 1;
  e       | enum_out
  m       | multirange_out
  r       | range_out
-(4 rows)
+ y       | byteaout
+(5 rows)
 
 -- Domains should have same typoutput as their base types
 SELECT t1.oid, t1.typname, t2.oid, t2.typname
@@ -335,7 +339,8 @@ ORDER BY 1;
  e       | enum_recv
  m       | multirange_recv
  r       | range_recv
-(5 rows)
+ y       | bytearecv
+(6 rows)
 
 -- Check for bogus typsend routines
 -- As of 7.4, this check finds refcursor, which is borrowing
@@ -380,7 +385,8 @@ ORDER BY 1;
  e       | enum_send
  m       | multirange_send
  r       | range_send
-(4 rows)
+ y       | byteasend
+(5 rows)
 
 -- Domains should have same typsend as their base types
 SELECT t1.oid, t1.typname, t2.oid, t2.typname
@@ -707,6 +713,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::pg_encrypted_rnd,
+  E'\\xDEADBEEF'::pg_encrypted_det,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 103e11483d..ffca206c6f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -129,6 +129,9 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # ----------
 test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats
 
+# WIP
+test: column_encryption
+
 # event_trigger cannot run concurrently with any test that runs DDL
 # oidjoins is read-only, though, and should run late for best coverage
 test: event_trigger oidjoins
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index a4b354c9e6..8ad1f458d5 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..37f0b23d10
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,50 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+/* Imagine:
+CREATE COLUMN MASTER KEY cmk1 (
+    realm = 'test'
+);
+*/
+INSERT INTO pg_colmasterkey (oid, cmkname, cmkowner, cmkrealm) VALUES (
+    pg_nextoid('pg_catalog.pg_colmasterkey', 'oid', 'pg_catalog.pg_colmasterkey_oid_index'),
+    'cmk1',
+    (select oid from pg_roles where rolname = current_user),
+    'test'
+);
+
+/* Imagine:
+CREATE COLUMN ENCRYPTION KEY cek1 (
+    column_master_key = cmk1,
+    algorithm = '...',
+    encrypted_value = '...'
+);
+*/
+INSERT INTO pg_colenckey (oid, cekname, cekowner) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckey', 'oid', 'pg_catalog.pg_colenckey_oid_index'),
+    'cek1',
+    (select oid from pg_roles where rolname = current_user)
+);
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = 'cek1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1'),
+    1,
+    '\xDEADBEEF'
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+
+CREATE TABLE tbl_29f3 (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+\d tbl_29f3
+\d+ tbl_29f3
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95c..7db788735c 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 1149c6a839..e88c70d302 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -119,6 +119,23 @@ CREATE TABLE bububu(a int) \gdesc
 -- all on one line
 SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g
 
+
+-- \gencr
+-- (This just tests the parameter passing; there is no encryption here.)
+
+CREATE TABLE test_gencr (a int, b text);
+INSERT INTO test_gencr VALUES (1, 'one') \gencr
+SELECT * FROM test_gencr WHERE a = 1 \gencr
+
+INSERT INTO test_gencr VALUES ($1, $2) \gencr 2 'two'
+SELECT * FROM test_gencr WHERE a IN ($1, $2) \gencr 2 3
+
+-- test parse error
+SELECT * FROM test_gencr WHERE a = x \gencr
+-- test bind error
+SELECT * FROM test_gencr WHERE a = $1 \gencr
+
+
 -- \gexec
 
 create temporary table gexec_test(a int, b text, c date, d float);
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 5edc1f1f6e..34dd19456d 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -20,7 +20,7 @@
 FROM pg_type as t1
 WHERE t1.typnamespace = 0 OR
     (t1.typlen <= 0 AND t1.typlen != -1 AND t1.typlen != -2) OR
-    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r')) OR
+    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT t1.typisdefined OR
     (t1.typalign not in ('c', 's', 'i', 'd')) OR
     (t1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -529,6 +529,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::pg_encrypted_rnd,
+  E'\\xDEADBEEF'::pg_encrypted_det,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 971a74cf22..db1f3b8811 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,6 +20,8 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 61cd591430..a0bd708132 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4cc129bebd..dddb27113f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 053294c99f..550a53649b 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,15 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	bool		attislocal BKI_DEFAULT(t);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey);
+
+	/* real type if encrypted */
+	Oid			attrealtypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+
+	/* encryption algorithm (PG_CEK_* values) */
+	int16		attencalg BKI_DEFAULT(0);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index 4471eb6bbe..878b0bcbbf 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -546,4 +546,10 @@
 { castsource => 'tstzrange', casttarget => 'tstzmultirange',
   castfunc => 'tstzmultirange(tstzrange)', castcontext => 'e',
   castmethod => 'f' },
+
+{ castsource => 'bytea', casttarget => 'pg_encrypted_det', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+{ castsource => 'bytea', casttarget => 'pg_encrypted_rnd', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+
 ]
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..095ed712b0
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,55 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+/* ----------------
+ *		pg_colenckey definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckey
+ * ----------------
+ */
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_index, 8242, ColumnEncKeyNameIndexId, on pg_colenckey using btree(cekname name_ops));
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  For clarity, the assigned numbers are not reused between CMKs
+ * and CEKs, but that is not technically required.  In either case, don't
+ * assign zero, so that that can be used as an invalid value.
+ */
+
+#define PG_CMK_RSAES_OAEP_SHA_1			1
+#define PG_CMK_RSAES_OAEP_SHA_256		2
+
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	130
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	131
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	132
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	133
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..3e4dea7218
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkeydata.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+/* ----------------
+ *		pg_colenckeydata definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckeydata
+ * ----------------
+ */
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+	int16		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..0344cc4201
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,45 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_index, 8241, ColumnMasterKeyNameIndexId, on pg_colmasterkey using btree(cmkname name_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index dbcae7ffdd..0ca401ffe4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index bc5f8213f3..4737a7f9ed 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index b3b6a7e616..5343580b06 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2e41f4d9e8..2fa26a2222 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8025,9 +8025,9 @@
   proname => 'pg_prepared_statement', prorows => '1000', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'record',
   proargtypes => '',
-  proallargtypes => '{text,text,timestamptz,_regtype,_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',
@@ -11886,4 +11886,11 @@
   prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary',
   prosrc => 'brin_minmax_multi_summary_send' },
 
+{ oid => '8245',
+  proname => 'pg_encrypted_det_eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'pg_encrypted_det_ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index df45879463..df1fa3e393 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,16 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 48a2559137..11aff9ff83 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -277,6 +277,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPTYPE_MULTIRANGE	'm' /* multirange type */
 #define  TYPTYPE_PSEUDO		'p' /* pseudo-type */
 #define  TYPTYPE_RANGE		'r' /* range type */
+#define  TYPTYPE_ENCRYPTED	'y' /* encrypted column value */
 
 #define  TYPCATEGORY_INVALID	'\0'	/* not an allowed category */
 #define  TYPCATEGORY_ARRAY		'A'
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f93d866548..8c08bd845f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -679,6 +679,7 @@ typedef struct ColumnDef
 	char	   *colname;		/* name of column */
 	TypeName   *typeName;		/* type of column */
 	char	   *compression;	/* compression method for column */
+	List	   *encryption;		/* encryption info for column */
 	int			inhcount;		/* number of times column is inherited */
 	bool		is_local;		/* column has local (non-inherited) def'n */
 	bool		is_not_null;	/* NOT NULL constraint specified? */
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index dc379547c7..c310e124f1 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -28,7 +28,9 @@ extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook;
 extern Query *parse_analyze_fixedparams(RawStmt *parseTree, const char *sourceText,
 										const Oid *paramTypes, int numParams, QueryEnvironment *queryEnv);
 extern Query *parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
-									  Oid **paramTypes, int *numParams, QueryEnvironment *queryEnv);
+									  Oid **paramTypes, int *numParams,
+									  Oid **paramOrigTbls, AttrNumber **paramOrigCols,
+									  QueryEnvironment *queryEnv);
 extern Query *parse_analyze_withcb(RawStmt *parseTree, const char *sourceText,
 								   ParserSetupHook parserSetup,
 								   void *parserSetupArg,
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index cf9c759025..225a2a8733 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -93,6 +93,7 @@ typedef Node *(*ParseParamRefHook) (ParseState *pstate, ParamRef *pref);
 typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param,
 								  Oid targetTypeId, int32 targetTypeMod,
 								  int location);
+typedef void (*ParamAssignOrigHook) (ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 
 
 /*
@@ -222,6 +223,7 @@ struct ParseState
 	PostParseColumnRefHook p_post_columnref_hook;
 	ParseParamRefHook p_paramref_hook;
 	CoerceParamHook p_coerce_param_hook;
+	ParamAssignOrigHook p_param_assign_orig_hook;
 	void	   *p_ref_hook_state;	/* common passthrough link for above */
 };
 
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index df1ee660d8..da202b28a7 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -18,7 +18,8 @@
 extern void setup_parse_fixed_parameters(ParseState *pstate,
 										 const Oid *paramTypes, int numParams);
 extern void setup_parse_variable_parameters(ParseState *pstate,
-											Oid **paramTypes, int *numParams);
+											Oid **paramTypes, int *numParams,
+											Oid **paramOrigTbls, AttrNumber **paramOrigCols);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
 
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 70d9dab25b..3038ceb522 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -53,6 +53,8 @@ extern List *pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 											  const char *query_string,
 											  Oid **paramTypes,
 											  int *numParams,
+											  Oid **paramOrigTbls,
+											  AttrNumber **paramOrigCols,
 											  QueryEnvironment *queryEnv);
 extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree,
 										   const char *query_string,
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 0499635f59..9a7cf794ce 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 4463ea66be..4e8a940590 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,12 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATAOID,
+	CEKNAME,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index e99aa279f6..79154f8aab 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -46,6 +46,8 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc)
 		pq_sendint16(&buf, attr->attlen);
 		pq_sendint32(&buf, attr->atttypmod);
 		pq_sendint16(&buf, 0);	/* format code */
+		pq_sendint32(&buf, 0);	/* CEK */
+		pq_sendint16(&buf, 0);	/* CEK alg */
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index d2f3b57288..28b437e5c7 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,26 @@
  */
 #include "postgres.h"
 
+#include "access/genam.h"
 #include "access/printtup.h"
+#include "access/skey.h"
+#include "access/table.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/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 +164,135 @@ 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;
+
+	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;
+
+	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
  *
@@ -200,6 +342,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 +375,22 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (get_typtype(atttypid) == TYPTYPE_ENCRYPTED)
+		{
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(resorigtbl), Int16GetDatum(resorigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", resorigcol, resorigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			atttypid = orig_att->attrealtypid;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->attencalg;
+			ReleaseSysCache(tp);
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +398,8 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		pq_writeint16(buf, att->attlen);
 		pq_writeint32(buf, atttypmod);
 		pq_writeint16(buf, format);
+		pq_writeint32(buf, attcekid);
+		pq_writeint16(buf, attencalg);
 	}
 
 	pq_endmessage_reuse(buf);
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index d6fb261e20..e3ecc64ea7 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attrealtypid != attr2->attrealtypid)
+			return false;
+		if (attr1->attencalg != attr2->attencalg)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 10bf26ce7c..cea91d4a88 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 89a0221ec9..7e1a91b974 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 1803194db9..0ea58239b4 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -746,6 +746,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attrealtypid - 1] = ObjectIdGetDatum(attrs->attrealtypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attencalg - 1] = Int16GetDatum(attrs->attencalg);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 2333aae467..94578350bd 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -50,7 +50,6 @@ static void InitQueryHashTable(void);
 static ParamListInfo EvaluateParams(ParseState *pstate,
 									PreparedStatement *pstmt, List *params,
 									EState *estate);
-static Datum build_regtype_array(Oid *param_types, int num_params);
 
 /*
  * Implements the 'PREPARE' utility statement.
@@ -62,6 +61,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	RawStmt    *rawstmt;
 	CachedPlanSource *plansource;
 	Oid		   *argtypes = NULL;
+	Oid		   *argorigtbls = NULL;
+	AttrNumber *argorigcols = NULL;
 	int			nargs;
 	List	   *query_list;
 
@@ -108,6 +109,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 
 			argtypes[i++] = toid;
 		}
+
+		argorigtbls = (Oid *) palloc0(nargs * sizeof(Oid));
+		argorigcols = (AttrNumber *) palloc0(nargs * sizeof(AttrNumber));
 	}
 
 	/*
@@ -117,7 +121,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
 	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
-												  &argtypes, &nargs, NULL);
+												  &argtypes, &nargs,
+												  &argorigtbls, &argorigcols,
+												  NULL);
 
 	/* Finish filling in the CachedPlanSource */
 	CompleteCachedPlan(plansource,
@@ -125,6 +131,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 					   NULL,
 					   argtypes,
 					   nargs,
+					   argorigtbls,
+					   argorigcols,
 					   NULL,
 					   NULL,
 					   CURSOR_OPT_PARALLEL_OK,	/* allow parallel mode */
@@ -683,9 +691,11 @@ 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];
+			Datum	   *tmp_ary;
+			Datum		values[10];
+			bool		nulls[10];
 
 			result_desc = prep_stmt->plansource->resultDesc;
 
@@ -694,25 +704,38 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 			values[0] = CStringGetTextDatum(prep_stmt->stmt_name);
 			values[1] = CStringGetTextDatum(prep_stmt->plansource->query_string);
 			values[2] = TimestampTzGetDatum(prep_stmt->prepare_time);
-			values[3] = build_regtype_array(prep_stmt->plansource->param_types,
-											prep_stmt->plansource->num_params);
+
+			tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_types[i]);
+			values[3] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGTYPEOID));
+
+			tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_origtbls[i]);
+			values[4] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGCLASSOID));
+
+			tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = Int16GetDatum(prep_stmt->plansource->param_origcols[i]);
+			values[5] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, INT2OID));
+
 			if (result_desc)
 			{
-				Oid		   *result_types;
-
-				result_types = (Oid *) palloc(result_desc->natts * sizeof(Oid));
+				tmp_ary = (Datum *) palloc(result_desc->natts * sizeof(Datum));
 				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);
@@ -721,24 +744,3 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 
 	return (Datum) 0;
 }
-
-/*
- * This utility function takes a C array of Oids, and returns a Datum
- * pointing to a one-dimensional Postgres array of regtypes. An empty
- * array is returned as a zero-element array, not NULL.
- */
-static Datum
-build_regtype_array(Oid *param_types, int num_params)
-{
-	Datum	   *tmp_ary;
-	ArrayType  *result;
-	int			i;
-
-	tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
-
-	for (i = 0; i < num_params; i++)
-		tmp_ary[i] = ObjectIdGetDatum(param_types[i]);
-
-	result = construct_array_builtin(tmp_ary, num_params, REGTYPEOID);
-	return PointerGetDatum(result);
-}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2de0ebacec..a0a9c6b59a 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
@@ -931,6 +932,76 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 		if (colDef->compression)
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
+
+		if (colDef->encryption)
+		{
+			ListCell   *lc;
+			char	   *cek = NULL;
+			Oid			cekoid;
+			bool		encdet = false;
+			int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+			foreach(lc, colDef->encryption)
+			{
+				DefElem    *el = lfirst_node(DefElem, lc);
+
+				if (strcmp(el->defname, "column_encryption_key") == 0)
+					cek = strVal(linitial(castNode(TypeName, el->arg)->names));
+				else if (strcmp(el->defname, "encryption_type") == 0)
+				{
+					char	   *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+					if (strcmp(val, "deterministic") == 0)
+						encdet = true;
+					else if (strcmp(val, "randomized") == 0)
+						encdet = false;
+					else
+						ereport(ERROR,
+								errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								errmsg("unrecognized encryption type: %s", val));
+				}
+				else if (strcmp(el->defname, "algorithm") == 0)
+				{
+					char	   *val = strVal(el->arg);
+
+					if (strcmp(val, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+						alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+					else if (strcmp(val, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+						alg = PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+					else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+						alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+					else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+						alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+					else
+						ereport(ERROR,
+								errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								errmsg("unrecognized encryption algorithm: %s", val));
+				}
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("unrecognized column encryption parameter: %s", el->defname));
+			}
+
+			if (!cek)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+						errmsg("column encryption key must be specified"));
+
+			cekoid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid, PointerGetDatum(cek));
+			if (!cekoid)
+				ereport(ERROR,
+						errcode(ERRCODE_UNDEFINED_OBJECT),
+						errmsg("column encryption key \"%s\" does not exist", cek));
+
+			attr->attcek = cekoid;
+			attr->attrealtypid = attr->atttypid;
+			if (encdet)
+				attr->atttypid = PG_ENCRYPTED_DETOID;
+			else
+				attr->atttypid = PG_ENCRYPTED_RNDOID;
+			attr->attencalg = alg;
+		}
 	}
 
 	/*
@@ -6829,6 +6900,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attgenerated = colDef->generated;
 	attribute.attisdropped = false;
 	attribute.attislocal = colDef->is_local;
+	attribute.attcek = 0; // TODO
+	attribute.attrealtypid = 0; // TODO
+	attribute.attencalg = 0; // TODO
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
 
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 29bc26669b..cde5b0e087 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2279,6 +2279,8 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan)
 						   NULL,
 						   plan->argtypes,
 						   plan->nargs,
+						   NULL,
+						   NULL,
 						   plan->parserSetup,
 						   plan->parserSetupArg,
 						   plan->cursor_options,
@@ -2516,6 +2518,8 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 							   NULL,
 							   plan->argtypes,
 							   plan->nargs,
+							   NULL,
+							   NULL,
 							   plan->parserSetup,
 							   plan->parserSetupArg,
 							   plan->cursor_options,
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 51d630fa89..c901cfbb86 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -3555,6 +3555,7 @@ _copyColumnDef(const ColumnDef *from)
 	COPY_STRING_FIELD(colname);
 	COPY_NODE_FIELD(typeName);
 	COPY_STRING_FIELD(compression);
+	COPY_NODE_FIELD(encryption);
 	COPY_SCALAR_FIELD(inhcount);
 	COPY_SCALAR_FIELD(is_local);
 	COPY_SCALAR_FIELD(is_not_null);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index e747e1667d..69a3adee7c 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -3045,6 +3045,7 @@ _equalColumnDef(const ColumnDef *a, const ColumnDef *b)
 	COMPARE_STRING_FIELD(colname);
 	COMPARE_NODE_FIELD(typeName);
 	COMPARE_STRING_FIELD(compression);
+	COMPARE_NODE_FIELD(encryption);
 	COMPARE_SCALAR_FIELD(inhcount);
 	COMPARE_SCALAR_FIELD(is_local);
 	COMPARE_SCALAR_FIELD(is_not_null);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 4cb1744da6..8fbe0f5f8f 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4288,6 +4288,8 @@ raw_expression_tree_walker(Node *node,
 					return true;
 				if (walker(coldef->compression, context))
 					return true;
+				if (walker(coldef->encryption, context))
+					return true;
 				if (walker(coldef->raw_default, context))
 					return true;
 				if (walker(coldef->collClause, context))
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 4315c53080..777eac7f62 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -3116,6 +3116,7 @@ _outColumnDef(StringInfo str, const ColumnDef *node)
 	WRITE_STRING_FIELD(colname);
 	WRITE_NODE_FIELD(typeName);
 	WRITE_STRING_FIELD(compression);
+	WRITE_NODE_FIELD(encryption);
 	WRITE_INT_FIELD(inhcount);
 	WRITE_BOOL_FIELD(is_local);
 	WRITE_BOOL_FIELD(is_not_null);
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 1bcb875507..edc9fb4a28 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -145,6 +145,7 @@ parse_analyze_fixedparams(RawStmt *parseTree, const char *sourceText,
 Query *
 parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
 						Oid **paramTypes, int *numParams,
+						Oid **paramOrigTbls, AttrNumber **paramOrigCols,
 						QueryEnvironment *queryEnv)
 {
 	ParseState *pstate = make_parsestate(NULL);
@@ -155,7 +156,7 @@ parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
 
 	pstate->p_sourcetext = sourceText;
 
-	setup_parse_variable_parameters(pstate, paramTypes, numParams);
+	setup_parse_variable_parameters(pstate, paramTypes, numParams, paramOrigTbls, paramOrigCols);
 
 	pstate->p_queryEnv = queryEnv;
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 969c9c158f..a918eaf427 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -596,6 +596,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableConstraint TableLikeClause
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_match
@@ -3778,13 +3779,14 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_compression opt_column_encryption create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 
 					n->colname = $1;
 					n->typeName = $2;
 					n->compression = $3;
+					n->encryption = $4;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3793,8 +3795,8 @@ columnDef:	ColId Typename opt_column_compression create_generic_options ColQualL
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $4;
-					SplitColQualList($5, &n->constraints, &n->collClause,
+					n->fdwoptions = $5;
+					SplitColQualList($6, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *) n;
@@ -3851,6 +3853,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 ColQualList:
 			ColQualList ColConstraint				{ $$ = lappend($1, $2); }
 			| /*EMPTY*/								{ $$ = NIL; }
diff --git a/src/backend/parser/parse_param.c b/src/backend/parser/parse_param.c
index f668abfcb3..b45bf4c438 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -49,6 +49,8 @@ typedef struct VarParamState
 {
 	Oid		  **paramTypes;		/* array of parameter type OIDs */
 	int		   *numParams;		/* number of array entries */
+	Oid		  **paramOrigTbls;	/* underlying tables (0 if none) */
+	AttrNumber **paramOrigCols; /* underlying columns (0 if none) */
 } VarParamState;
 
 static Node *fixed_paramref_hook(ParseState *pstate, ParamRef *pref);
@@ -56,6 +58,7 @@ static Node *variable_paramref_hook(ParseState *pstate, ParamRef *pref);
 static Node *variable_coerce_param_hook(ParseState *pstate, Param *param,
 										Oid targetTypeId, int32 targetTypeMod,
 										int location);
+static void variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 static bool check_parameter_resolution_walker(Node *node, ParseState *pstate);
 static bool query_contains_extern_params_walker(Node *node, void *context);
 
@@ -81,15 +84,19 @@ setup_parse_fixed_parameters(ParseState *pstate,
  */
 void
 setup_parse_variable_parameters(ParseState *pstate,
-								Oid **paramTypes, int *numParams)
+								Oid **paramTypes, int *numParams,
+								Oid **paramOrigTbls, AttrNumber **paramOrigCols)
 {
 	VarParamState *parstate = palloc(sizeof(VarParamState));
 
 	parstate->paramTypes = paramTypes;
 	parstate->numParams = numParams;
+	parstate->paramOrigTbls = paramOrigTbls;
+	parstate->paramOrigCols = paramOrigCols;
 	pstate->p_ref_hook_state = (void *) parstate;
 	pstate->p_paramref_hook = variable_paramref_hook;
 	pstate->p_coerce_param_hook = variable_coerce_param_hook;
+	pstate->p_param_assign_orig_hook = variable_param_assign_orig_hook;
 }
 
 /*
@@ -145,14 +152,36 @@ variable_paramref_hook(ParseState *pstate, ParamRef *pref)
 	{
 		/* Need to enlarge param array */
 		if (*parstate->paramTypes)
+		{
 			*parstate->paramTypes = (Oid *) repalloc(*parstate->paramTypes,
 													 paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc(*parstate->paramOrigTbls,
+													paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc(*parstate->paramOrigCols,
+													paramno * sizeof(AttrNumber));
+		}
 		else
+		{
 			*parstate->paramTypes = (Oid *) palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc(paramno * sizeof(AttrNumber));
+		}
 		/* Zero out the previously-unreferenced slots */
 		MemSet(*parstate->paramTypes + *parstate->numParams,
 			   0,
 			   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigTbls)
+			MemSet(*parstate->paramOrigTbls + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigCols)
+			MemSet(*parstate->paramOrigCols + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(AttrNumber));
 		*parstate->numParams = paramno;
 	}
 
@@ -260,6 +289,18 @@ variable_coerce_param_hook(ParseState *pstate, Param *param,
 	return NULL;
 }
 
+static void
+variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol)
+{
+	VarParamState *parstate = (VarParamState *) pstate->p_ref_hook_state;
+	int			paramno = param->paramid;
+
+	if (parstate->paramOrigTbls)
+		(*parstate->paramOrigTbls)[paramno - 1] = origtbl;
+	if (parstate->paramOrigCols)
+		(*parstate->paramOrigCols)[paramno - 1] = origcol;
+}
+
 /*
  * Check for consistent assignment of variable parameters after completion
  * of parsing with parse_variable_parameters.
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 2a1d44b813..b964088044 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -595,6 +595,12 @@ transformAssignedExpr(ParseState *pstate,
 					 parser_errposition(pstate, exprLocation(orig_expr))));
 	}
 
+	if (IsA(expr, Param))
+	{
+		if (pstate->p_param_assign_orig_hook)
+			pstate->p_param_assign_orig_hook(pstate, castNode(Param, expr), RelationGetRelid(pstate->p_target_relation), attrno);
+	}
+
 	pstate->p_expr_kind = sv_expr_kind;
 
 	return expr;
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 495cbf2006..82168235e8 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -79,6 +79,7 @@
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
 
@@ -680,6 +681,8 @@ pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 								 const char *query_string,
 								 Oid **paramTypes,
 								 int *numParams,
+								 Oid **paramOrigTbls,
+								 AttrNumber **paramOrigCols,
 								 QueryEnvironment *queryEnv)
 {
 	Query	   *query;
@@ -694,7 +697,7 @@ pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 		ResetUsage();
 
 	query = parse_analyze_varparams(parsetree, query_string, paramTypes, numParams,
-									queryEnv);
+									paramOrigTbls, paramOrigCols, queryEnv);
 
 	/*
 	 * Check all parameter types got determined.
@@ -1371,6 +1374,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	bool		is_named;
 	bool		save_log_statement_stats = log_statement_stats;
 	char		msec_str[32];
+	Oid		   *paramOrigTbls = palloc(numParams * sizeof(Oid));
+	AttrNumber *paramOrigCols = palloc(numParams * sizeof(AttrNumber));
 
 	/*
 	 * Report query to various monitoring facilities.
@@ -1491,6 +1496,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 														  query_string,
 														  &paramTypes,
 														  &numParams,
+														  &paramOrigTbls,
+														  &paramOrigCols,
 														  NULL);
 
 		/* Done with the snapshot used for parsing */
@@ -1521,6 +1528,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 					   unnamed_stmt_context,
 					   paramTypes,
 					   numParams,
+					   paramOrigTbls,
+					   paramOrigCols,
 					   NULL,
 					   NULL,
 					   CURSOR_OPT_PARALLEL_OK,	/* allow parallel mode */
@@ -1818,6 +1827,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter corresponds to an encrypted column, but the parameter value was not encrypted")));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2614,8 +2633,41 @@ exec_describe_statement_message(const char *stmt_name)
 	for (int i = 0; i < psrc->num_params; i++)
 	{
 		Oid			ptype = psrc->param_types[i];
+		Oid			pcekid = InvalidOid;
+		int			pcekalg = 0;
+		int16		pflags = 0;
+
+		if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+		{
+			Oid			porigtbl = psrc->param_origtbls[i];
+			AttrNumber	porigcol = psrc->param_origcols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol <= 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("parameter %d corresponds to an encrypted column, but an underlying table and column could not be determined", i)));
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(porigtbl), Int16GetDatum(porigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", porigcol, porigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			ptype = orig_att->attrealtypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->attencalg;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x01;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		pq_sendint32(&row_description_buf, (int) pcekid);
+		pq_sendint16(&row_description_buf, pcekalg);
+		pq_sendint16(&row_description_buf, pflags);
 	}
 	pq_endmessage_reuse(&row_description_buf);
 
diff --git a/src/backend/utils/adt/arrayfuncs.c b/src/backend/utils/adt/arrayfuncs.c
index b0c37ede87..5683731188 100644
--- a/src/backend/utils/adt/arrayfuncs.c
+++ b/src/backend/utils/adt/arrayfuncs.c
@@ -3387,6 +3387,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/plancache.c b/src/backend/utils/cache/plancache.c
index 0d6a295674..f6893dd8b7 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -340,6 +340,8 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 				   MemoryContext querytree_context,
 				   Oid *param_types,
 				   int num_params,
+				   Oid *param_origtbls,
+				   AttrNumber *param_origcols,
 				   ParserSetupHook parserSetup,
 				   void *parserSetupArg,
 				   int cursor_options,
@@ -419,6 +421,14 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 	{
 		plansource->param_types = (Oid *) palloc(num_params * sizeof(Oid));
 		memcpy(plansource->param_types, param_types, num_params * sizeof(Oid));
+
+		plansource->param_origtbls = (Oid *) palloc0(num_params * sizeof(Oid));
+		if (param_origtbls)
+			memcpy(plansource->param_origtbls, param_origtbls, num_params * sizeof(Oid));
+
+		plansource->param_origcols = (AttrNumber *) palloc0(num_params * sizeof(AttrNumber));
+		if (param_origcols)
+			memcpy(plansource->param_origcols, param_origcols, num_params * sizeof(AttrNumber));
 	}
 	else
 		plansource->param_types = NULL;
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 1912b12146..468a6873e4 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,33 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		256
 	},
+	{
+		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 +319,15 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		8
 	},
+	{
+		ColumnMasterKeyRelationId,	/* CMKOID */
+		ColumnMasterKeyOidIndexId,
+		1,
+		{
+			Anum_pg_colmasterkey_oid,
+		},
+		8
+	},
 	{CollationRelationId,		/* COLLNAMEENCNSP */
 		CollationNameEncNspIndexId,
 		3,
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c871cb727d..962947c78d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8066,6 +8066,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcekname;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8175,17 +8176,24 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	if (fout->remoteVersion >= 120000)
 		appendPQExpBufferStr(q,
-							 "a.attgenerated\n");
+							 "a.attgenerated,\n");
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "'' AS attgenerated,\n");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBufferStr(q,
+							 "(SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek ) AS attcekname\n");
+	else
+		appendPQExpBufferStr(q,
+							 "NULL AS attcekname\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
 					  "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
 					  "JOIN pg_catalog.pg_attribute a ON (src.tbloid = a.attrelid) "
 					  "LEFT JOIN pg_catalog.pg_type t "
-					  "ON (a.atttypid = t.oid)\n"
+					  "ON (CASE WHEN a.attrealtypid <> 0 THEN a.attrealtypid ELSE a.atttypid END = t.oid)\n"
 					  "WHERE a.attnum > 0::pg_catalog.int2\n"
 					  "ORDER BY a.attrelid, a.attnum",
 					  tbloids->data);
@@ -8204,6 +8212,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcekname = PQfnumber(res, "attcekname");
 	i_attisdropped = PQfnumber(res, "attisdropped");
 	i_attlen = PQfnumber(res, "attlen");
 	i_attalign = PQfnumber(res, "attalign");
@@ -8265,6 +8274,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		tbinfo->typstorage = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attidentity = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attgenerated = (char *) pg_malloc(numatts * sizeof(char));
+		tbinfo->attcekname = (char **) pg_malloc(numatts * sizeof(char *));
 		tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char));
@@ -8293,6 +8303,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->attidentity[j] = *(PQgetvalue(res, r, i_attidentity));
 			tbinfo->attgenerated[j] = *(PQgetvalue(res, r, i_attgenerated));
 			tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
+			if (!PQgetisnull(res, r, i_attcekname))
+				tbinfo->attcekname[j] = pg_strdup(PQgetvalue(res, r, i_attcekname));
+			else
+				tbinfo->attcekname[j] = NULL;
 			tbinfo->attisdropped[j] = (PQgetvalue(res, r, i_attisdropped)[0] == 't');
 			tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
 			tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
@@ -15260,6 +15274,12 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcekname[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s)",
+										  fmtId(tbinfo->attcekname[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..2c176ed13c 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -333,6 +333,7 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	char	  **attcekname;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
 	char	   *attalign;		/* attribute align, used by binary_upgrade */
 	bool	   *attislocal;		/* true if attr has local definition */
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index c562c04afe..72e973d2de 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 9f95869eca..8fe055ffed 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1284,6 +1284,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;
 
@@ -1448,7 +1456,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 88d92a08ae..fe81547b29 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1498,7 +1498,7 @@ describeOneTableDetails(const char *schemaname,
 	bool		printTableInitialized = false;
 	int			i;
 	char	   *view_def = NULL;
-	char	   *headers[12];
+	char	   *headers[13];
 	PQExpBufferData title;
 	PQExpBufferData tmpbuf;
 	int			cols;
@@ -1514,6 +1514,7 @@ describeOneTableDetails(const char *schemaname,
 				fdwopts_col = -1,
 				attstorage_col = -1,
 				attcompression_col = -1,
+				attcekname_col = -1,
 				attstattarget_col = -1,
 				attdescr_col = -1;
 	int			numrows;
@@ -1812,7 +1813,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)
@@ -1826,7 +1827,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");
@@ -1877,6 +1878,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 ||
@@ -2000,6 +2010,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)
@@ -2092,6 +2104,17 @@ describeOneTableDetails(const char *schemaname,
 							  false, false);
 		}
 
+		/* Column encryption */
+		if (attcekname_col >= 0)
+		{
+			if (!PQgetisnull(res, i, attcekname_col))
+				printTableAddCell(&cont, PQgetvalue(res, i, attcekname_col),
+								  false, false);
+			else
+				printTableAddCell(&cont, "",
+								  false, false);
+		}
+
 		/* Statistics target, if the relkind supports this feature */
 		if (attstattarget_col >= 0)
 			printTableAddCell(&cont, PQgetvalue(res, i, attstattarget_col),
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index f8ce1a0706..9f4fce26fd 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -195,6 +195,7 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\g [(OPTIONS)] [FILE]  execute query (and send result to file or |pipe);\n"
 		  "                         \\g with no arguments is equivalent to a semicolon\n");
 	HELP0("  \\gdesc                 describe result of query, without executing it\n");
+	HELP0("  \\gencr [PARAM]...      execute query, with support for parameter encryption\n");
 	HELP0("  \\gexec                 execute query, then execute each value in its result\n");
 	HELP0("  \\gset [PREFIX]         execute query and store result in psql variables\n");
 	HELP0("  \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n");
@@ -412,6 +413,8 @@ helpVariables(unsigned short int pager)
 		  "    true if last query failed, else false\n");
 	HELP0("  FETCH_COUNT\n"
 		  "    the number of result rows to fetch and display at a time (0 = unlimited)\n");
+	HELP0("  HIDE_COLUMN_ENCRYPTION\n"
+		  "    if set, column encryption details are not displayed\n");
 	HELP0("  HIDE_TABLEAM\n"
 		  "    if set, table access methods are not displayed\n");
 	HELP0("  HIDE_TOAST_COMPRESSION\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 2399cffa3f..636729df1d 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -95,6 +95,10 @@ typedef struct _psqlSettings
 
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
 	bool		gdesc_flag;		/* one-shot request to describe query result */
+	bool		gencr_flag;		/* one-shot request to send query with support
+								 * for parameter encryption */
+	int			num_params;		/* number of query parameters */
+	char	  **params;			/* query parameters */
 	bool		gexec_flag;		/* one-shot request to execute query result */
 	bool		crosstab_flag;	/* one-shot request to crosstab result */
 	char	   *ctv_args[4];	/* \crosstabview arguments */
@@ -134,6 +138,7 @@ typedef struct _psqlSettings
 	bool		quiet;
 	bool		singleline;
 	bool		singlestep;
+	bool		hide_column_encryption;
 	bool		hide_compression;
 	bool		hide_tableam;
 	int			fetch_count;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 7c2f555f15..c955222a50 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -1188,6 +1188,13 @@ hide_compression_hook(const char *newval)
 							 &pset.hide_compression);
 }
 
+static bool
+hide_column_encryption_hook(const char *newval)
+{
+	return ParseVariableBool(newval, "HIDE_COLUMN_ENCRYPTION",
+							 &pset.hide_column_encryption);
+}
+
 static bool
 hide_tableam_hook(const char *newval)
 {
@@ -1259,6 +1266,9 @@ EstablishVariableSpace(void)
 	SetVariableHooks(pset.vars, "SHOW_CONTEXT",
 					 show_context_substitute_hook,
 					 show_context_hook);
+	SetVariableHooks(pset.vars, "HIDE_COLUMN_ENCRYPTION",
+					 bool_substitute_hook,
+					 hide_column_encryption_hook);
 	SetVariableHooks(pset.vars, "HIDE_TOAST_COMPRESSION",
 					 bool_substitute_hook,
 					 hide_compression_hook);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index c5cafe6f4b..d5877dc5a3 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1692,7 +1692,7 @@ psql_completion(const char *text, int start, int end)
 		"\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding",
 		"\\endif", "\\errverbose", "\\ev",
 		"\\f",
-		"\\g", "\\gdesc", "\\getenv", "\\gexec", "\\gset", "\\gx",
+		"\\g", "\\gdesc", "\\gencr", "\\getenv", "\\gexec", "\\gset", "\\gx",
 		"\\help", "\\html",
 		"\\if", "\\include", "\\include_relative", "\\ir",
 		"\\list", "\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index b5fd72a4ac..5a065eb80b 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..fabad38ac0 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,7 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPrepared2           187
+PQsendQueryPrepared2      188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index dc49387d6c..f3c15ece4d 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -344,6 +344,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	{"cmklookup", "PGCMKLOOKUP", "", NULL,
+		"CMK-Lookup", "", 64,
+	offsetof(struct pg_conn, cmklookup)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -4097,6 +4101,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 0000000000..524ba8a0f9
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,758 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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;
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, mac_key, mac_key_len);
+	if (!pkey)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("could not allocate key for HMAC: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out out memory");
+		goto fail;
+	}
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(cekalg);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	*(int64 *) (buf + PG_AD_LEN + encrlen) = pg_hton64(PG_AD_LEN * 8);
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, buf, bufsize))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, md_len_p))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	Assert(*md_len_p == md_hash_length(md));
+
+	/* truncate output to half the length, per spec */
+	*md_len_p /= 2;
+
+	result = true;
+fail:
+	free(buf);
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+
+unsigned char *
+decrypt_value(PGresult *res, const PGCEK *cek, int cekalg, const unsigned char *input, int inputlen, const char **errmsgp)
+{
+	static char msgbuf[1024];
+
+	const unsigned char *iv = NULL;
+	size_t		ivlen;
+
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *decr;
+	int			decrlen,
+				decrlen2;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized encryption algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized digest algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("column encryption key has wrong length for algorithm (has: %zu, required: %d)"),
+				 cek->cekdatalen, key_len);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key = cek->cekdata + mac_key_len;
+	mac_key = cek->cekdata;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  cekalg,
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	if (memcmp(input + (inputlen - md_len), md_value, md_len) != 0)
+	{
+		*errmsgp = libpq_gettext("MAC mismatch");
+		goto fail;
+	}
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	iv = input;
+	input += ivlen;
+	inputlen -= ivlen;
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = inputlen + EVP_CIPHER_CTX_block_size(evp_cipher_ctx) + 1;
+#ifndef TEST_ENCRYPT
+	buf = pqResultAlloc(res, bufsize, false);
+#else
+	buf = malloc(bufsize);
+#endif
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out out memory");
+		goto fail;
+	}
+	decr = buf;
+	if (!EVP_DecryptUpdate(evp_cipher_ctx, decr, &decrlen, input, inputlen - md_len))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	if (!EVP_DecryptFinal_ex(evp_cipher_ctx, decr + decrlen, &decrlen2))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	decrlen += decrlen2;
+	Assert(decrlen < bufsize);
+	decr[decrlen] = '\0';
+	result = decr;
+
+fail:
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	return result;
+}
+
+#ifndef TEST_ENCRYPT
+static bool
+make_siv(PGconn *conn,
+		 unsigned char *iv, size_t ivlen,
+		 const EVP_MD *md,
+		 const unsigned char *iv_key, int iv_key_len,
+		 const unsigned char *plaintext, int plaintext_len)
+{
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	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 0000000000..b0093dc527
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * encryption support
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FE_ENCRYPT_H
+#define FE_ENCRYPT_H
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+extern unsigned char *decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg,
+											int fromlen, const unsigned char *from,
+											int *tolen);
+
+extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg,
+									const unsigned char *input, int inputlen,
+									const char **errmsgp);
+
+extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg,
+									const unsigned char *value, int *nbytesp, bool enc_det);
+
+#endif							/* FE_ENCRYPT_H */
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 51e9a362f6..2e3c0295eb 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,381 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+int
+pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+					  const char *keyrealm)
+{
+	char	   *keyname_copy;
+	char	   *keyrealm_copy;
+	bool		found;
+
+	keyname_copy = strdup(keyname);
+	if (!keyname_copy)
+		return EOF;
+	keyrealm_copy = strdup(keyrealm);
+	if (!keyrealm_copy)
+	{
+		free(keyname_copy);
+		return EOF;
+	}
+
+	found = false;
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		struct pg_cmk *checkcmk = &conn->cmks[i];
+
+		/* replace existing? */
+		if (checkcmk->cmkid == keyid)
+		{
+			free(checkcmk->cmkname);
+			free(checkcmk->cmkrealm);
+			checkcmk->cmkname = keyname_copy;
+			checkcmk->cmkrealm = keyrealm_copy;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newncmks;
+		struct pg_cmk *newcmks;
+		struct pg_cmk *newcmk;
+
+		newncmks = conn->ncmks + 1;
+		if (newncmks <= 0)
+			return EOF;
+		newcmks = realloc(conn->cmks, newncmks * sizeof(struct pg_cmk));
+		if (!newcmks)
+		{
+			free(keyname_copy);
+			free(keyrealm_copy);
+			return EOF;
+		}
+
+		newcmk = &newcmks[newncmks - 1];
+		newcmk->cmkid = keyid;
+		newcmk->cmkname = keyname_copy;
+		newcmk->cmkrealm = keyrealm_copy;
+
+		conn->ncmks = newncmks;
+		conn->cmks = newcmks;
+	}
+
+	return 0;
+}
+
+/*
+ * Replace placeholders in input string.  Return value malloc'ed.
+ */
+static char *
+replace_cmk_placeholders(const char *in, const char *cmkname, const char *cmkrealm, int cmkalg, const char *b64data)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s;
+
+						switch (cmkalg)
+						{
+							case PG_CMK_RSAES_OAEP_SHA_1:
+								s = "RSAES_OAEP_SHA_1";
+								break;
+							case PG_CMK_RSAES_OAEP_SHA_256:
+								s = "RSAES_OAEP_SHA_256";
+								break;
+							default:
+								s = "INVALID";
+						}
+						appendPQExpBufferStr(&buf, s);
+					}
+					p++;
+					break;
+				case 'b':
+					appendPQExpBufferStr(&buf, b64data);
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'r':
+					appendPQExpBufferStr(&buf, cmkrealm);
+					p++;
+					break;
+				default:
+					appendPQExpBufferChar(&buf, p[0]);
+			}
+		}
+		else
+			appendPQExpBufferChar(&buf, p[0]);
+	}
+
+	return buf.data;
+}
+
+#ifndef USE_SSL
+/*
+ * Dummy implementation for non-SSL builds
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	appendPQExpBuffer(&conn->errorMessage,
+					  libpq_gettext("column encryption not supported by this build\n"));
+	return NULL;
+}
+#endif
+
+/*
+ * Decrypt a CEK using the given CMK.  The ciphertext is passed in
+ * "from" and "fromlen".  Return the decrypted value in a malloc'ed area, its
+ * length via "tolen".  Return NULL on error; add error messages directly to
+ * "conn".
+ */
+static unsigned char *
+decrypt_cek(PGconn *conn, const PGCMK *cmk, int cmkalg,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	char	   *cmklookup;
+	bool		found = false;
+	unsigned char *result = NULL;
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+		return NULL;
+	}
+
+	/*
+	 * Analyze semicolon-separated list
+	 */
+	for (char *s = strtok(cmklookup, ";"); s; s = strtok(NULL, ";"))
+	{
+		char	   *sep;
+
+		/* split found token at '=' */
+		sep = strchr(s, '=');
+		if (!sep)
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("syntax error in CMK lookup specification, missing \"%c\": %s\n"), '=', s);
+			break;
+		}
+
+		/* matching realm? */
+		if (strncmp(s, "*", sep - s) == 0 || strncmp(s, cmk->cmkrealm, sep - s) == 0)
+		{
+			char	   *sep2;
+
+			found = true;
+
+			sep2 = strchr(sep, ':');
+			if (!sep2)
+			{
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("syntax error in CMK lookup specification, missing \"%c\": %s\n"), ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+				FILE	   *fp;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				fp = fopen(cmkfilename, "rb");
+				if (fp)
+				{
+					result = decrypt_cek_from_file(conn, fp, cmkalg, fromlen, from, tolen);
+					fclose(fp);
+				}
+				else
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not open file \"%s\": %m\n"), cmkfilename);
+					free(cmkfilename);
+					goto fail;
+				}
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				int			enclen;
+				char	   *enc;
+				char	   *command;
+				FILE	   *fp;
+				char		buf[4096];
+
+				enclen = pg_b64_enc_len(fromlen);
+				enc = malloc(enclen + 1);
+				if (!enc)
+				{
+					appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+					goto fail;
+				}
+				enclen = pg_b64_encode((const char *) from, fromlen, enc, enclen);
+				if (enclen < 0)
+				{
+					appendPQExpBuffer(&conn->errorMessage, libpq_gettext("base64 encoding failed\n"));
+					free(enc);
+					goto fail;
+				}
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, enc);
+				free(enc);
+				fp = popen(command, "r");
+				free(command);
+				if (!fp)
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not run command \"%s\": %m\n"), command);
+					goto fail;
+				}
+				if (fgets(buf, sizeof(buf), fp))
+				{
+					int			linelen;
+					int			declen;
+					char	   *dec;
+
+					linelen = strlen(buf);
+					if (buf[linelen - 1] == '\n')
+					{
+						buf[linelen - 1] = '\0';
+						linelen--;
+					}
+					declen = pg_b64_dec_len(linelen);
+					dec = malloc(declen);
+					if (!dec)
+					{
+						appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+						goto fail;
+					}
+					declen = pg_b64_decode(buf, linelen, dec, declen);
+					if (declen < 0)
+					{
+						appendPQExpBuffer(&conn->errorMessage,
+										  libpq_gettext("base64 decoding failed\n"));
+						free(dec);
+						goto fail;
+					}
+					result = (unsigned char *) dec;
+					*tolen = declen;
+				}
+				else
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not read from command: %m\n"));
+					pclose(fp);
+					goto fail;
+				}
+				pclose(fp);
+			}
+			else
+			{
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("CMK lookup scheme \"%s\" not recognized\n"), sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("no CMK lookup found for realm \"%s\"\n"), cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+int
+pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, const unsigned char *value, int len)
+{
+	PGCMK	   *cmk = NULL;
+	unsigned char *plainval = NULL;
+	int			plainvallen = 0;
+	bool		found;
+
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		if (conn->cmks[i].cmkid == cmkid)
+		{
+			cmk = &conn->cmks[i];
+			break;
+		}
+	}
+
+	if (!cmk)
+		return EOF;
+
+	plainval = decrypt_cek(conn, cmk, cmkalg, len, value, &plainvallen);
+	if (!plainval)
+		return EOF;
+
+	found = false;
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		struct pg_cek *checkcek = &conn->ceks[i];
+
+		/* replace existing? */
+		if (checkcek->cekid == keyid)
+		{
+			free(checkcek->cekdata);
+			checkcek->cekdata = plainval;
+			checkcek->cekdatalen = plainvallen;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newnceks;
+		struct pg_cek *newceks;
+		struct pg_cek *newcek;
+
+		newnceks = conn->nceks + 1;
+		if (newnceks <= 0)
+		{
+			free(plainval);
+			return EOF;
+		}
+		newceks = realloc(conn->ceks, newnceks * sizeof(struct pg_cek));
+		if (!newceks)
+		{
+			free(plainval);
+			return EOF;
+		}
+
+		newcek = &newceks[newnceks - 1];
+		newcek->cekid = keyid;
+		newcek->cekdata = plainval;
+		newcek->cekdatalen = plainvallen;
+
+		conn->nceks = newnceks;
+		conn->ceks = newceks;
+	}
+
+	return 0;
+}
 
 /*
  * pqRowProcessor
@@ -1250,9 +1630,58 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 		}
 		else
 		{
-			bool		isbinary = (res->attDescs[i].format != 0);
+			bool		isbinary = ((res->attDescs[i].format & 0x0F) != 0);
 			char	   *val;
 
+			if (res->attDescs[i].cekid)
+			{
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == res->attDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					*errmsgp = libpq_gettext("column encryption key not found");
+					goto fail;
+				}
+
+				if (isbinary)
+				{
+					val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+												 (const unsigned char *) columns[i].value, clen, errmsgp);
+				}
+				else
+				{
+					unsigned char *buf;
+					unsigned char *enccolval;
+					size_t		enccolvallen;
+
+					buf = malloc(clen + 1);
+					memcpy(buf, columns[i].value, clen);
+					buf[clen] = '\0';
+					enccolval = PQunescapeBytea(buf, &enccolvallen);
+					val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+												 enccolval, enccolvallen, errmsgp);
+					free(enccolval);
+					free(buf);
+				}
+
+				if (val == NULL)
+					goto fail;
+#else
+				*errmsgp = libpq_gettext("column encryption not supported by this build");
+				goto fail;
+#endif
+			}
+			else
+			{
 			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
 			if (val == NULL)
 				goto fail;
@@ -1260,6 +1689,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;
@@ -1564,7 +1994,9 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   NULL,
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1682,6 +2114,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;
@@ -1709,7 +2155,9 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   paramForceColumnEncryptions,
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1808,7 +2256,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;
@@ -1855,14 +2305,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;
 		}
 	}
@@ -1881,6 +2370,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1899,9 +2389,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
 		{
@@ -2334,12 +2869,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);
 }
@@ -3602,6 +4152,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)
 {
@@ -3787,6 +4348,17 @@ PQparamtype(const PGresult *res, int param_num)
 		return InvalidOid;
 }
 
+int
+PQparamisencrypted(const PGresult *res, int param_num)
+{
+	if (!check_param_number(res, param_num))
+		return false;
+	if (res->paramDescs)
+		return (res->paramDescs[param_num].cekid != 0);
+	else
+		return false;
+}
+
 
 /* PQsetnonblocking:
  *	sets the PGconn's database connection non-blocking if the arg is true
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 10c76daf6e..9461c04bbe 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -45,6 +45,8 @@ static int	getRowDescriptions(PGconn *conn, int msgLength);
 static int	getParamDescriptions(PGconn *conn, int msgLength);
 static int	getAnotherTuple(PGconn *conn, int msgLength);
 static int	getParameterStatus(PGconn *conn);
+static int	getColumnMasterKey(PGconn *conn);
+static int	getColumnEncryptionKey(PGconn *conn);
 static int	getNotify(PGconn *conn);
 static int	getCopyStart(PGconn *conn, ExecStatusType copytype);
 static int	getReadyForQuery(PGconn *conn);
@@ -315,6 +317,22 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					if (getColumnMasterKey(conn))
+					{
+						// TODO: review error handling here
+						pqSaveErrorResult(conn);
+						pqInternalNotice(&conn->noticeHooks, "%s", conn->errorMessage.data);
+					}
+					break;
+				case 'Y':		/* Column Encryption Key */
+					if (getColumnEncryptionKey(conn))
+					{
+						// TODO: review error handling here
+						pqSaveErrorResult(conn);
+						pqInternalNotice(&conn->noticeHooks, "%s", conn->errorMessage.data);
+					}
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -572,6 +590,8 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		int			typlen;
 		int			atttypmod;
 		int			format;
+		int			cekid;
+		int			cekalg;
 
 		if (pqGets(&conn->workBuffer, conn) ||
 			pqGetInt(&tableid, 4, conn) ||
@@ -579,7 +599,9 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			pqGetInt(&typid, 4, conn) ||
 			pqGetInt(&typlen, 2, conn) ||
 			pqGetInt(&atttypmod, 4, conn) ||
-			pqGetInt(&format, 2, conn))
+			pqGetInt(&format, 2, conn) ||
+			pqGetInt(&cekid, 4, conn) ||
+			pqGetInt(&cekalg, 2, conn))
 		{
 			/* We should not run out of data here, so complain */
 			errmsg = libpq_gettext("insufficient data in \"T\" message");
@@ -607,8 +629,10 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		result->attDescs[i].typid = typid;
 		result->attDescs[i].typlen = typlen;
 		result->attDescs[i].atttypmod = atttypmod;
+		result->attDescs[i].cekid = cekid;
+		result->attDescs[i].cekalg = cekalg;
 
-		if (format != 1)
+		if ((format & 0x0F) != 1)
 			result->binary = 0;
 	}
 
@@ -710,10 +734,22 @@ getParamDescriptions(PGconn *conn, int msgLength)
 	for (i = 0; i < nparams; i++)
 	{
 		int			typid;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGetInt(&typid, 4, conn))
 			goto not_enough_data;
 		result->paramDescs[i].typid = typid;
+		if (pqGetInt(&cekid, 4, conn))
+			goto not_enough_data;
+		result->paramDescs[i].cekid = cekid;
+		if (pqGetInt(&cekalg, 2, conn))
+			goto not_enough_data;
+		result->paramDescs[i].cekalg = cekalg;
+		if (pqGetInt(&flags, 2, conn))
+			goto not_enough_data;
+		result->paramDescs[i].flags = flags;
 	}
 
 	/* Success! */
@@ -1439,6 +1475,89 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+/*
+ * Attempt to read a ColumnMasterKey message.
+ * Entry: 'y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	int			keyid;
+	char	   *keyname;
+	char	   *keyrealm;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	if (!keyname)
+		return EOF;
+	/* Get the key realm */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyrealm = strdup(conn->workBuffer.data);
+	if (!keyrealm)
+		return EOF;
+	/* And save it */
+	ret = pqSaveColumnMasterKey(conn, keyid, keyname, keyrealm);
+
+	free(keyname);
+	free(keyrealm);
+
+	return ret;
+}
+
+/*
+ * Attempt to read a ColumnEncryptionKey message.
+ * Entry: 'Y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	int			keyid;
+	int			cmkid;
+	int			cmkalg;
+	char	   *buf;
+	int			vallen;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK ID */
+	if (pqGetInt(&cmkid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK algorithm */
+	if (pqGetInt(&cmkalg, 2, conn) != 0)
+		return EOF;
+	/* Get the key data len */
+	if (pqGetInt(&vallen, 4, conn) != 0)
+		return EOF;
+	/* Get the key data */
+	buf = malloc(vallen);
+	if (!buf)
+		return EOF;
+	if (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	if (pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	free(buf);
+	return 0;
+}
 
 /*
  * Attempt to read a Notify response message.
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index 5d68cf2eb3..e08cbcff33 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -458,7 +458,12 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 	nfields = pqTraceOutputInt16(f, message, cursor);
 
 	for (int i = 0; i < nfields; i++)
+	{
+		pqTraceOutputInt32(f, message, cursor, regress);
 		pqTraceOutputInt32(f, message, cursor, regress);
+		pqTraceOutputInt16(f, message, cursor);
+		pqTraceOutputInt16(f, message, cursor);
+	}
 }
 
 /* RowDescription */
@@ -479,6 +484,8 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
 		pqTraceOutputInt16(f, message, cursor);
 		pqTraceOutputInt32(f, message, cursor, false);
 		pqTraceOutputInt16(f, message, cursor);
+		pqTraceOutputInt32(f, message, cursor, regress);
+		pqTraceOutputInt16(f, message, cursor);
 	}
 }
 
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 7986445f1a..42fbe0cd49 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -267,6 +267,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -438,6 +440,15 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPrepared2(PGconn *conn,
+								 const char *stmtName,
+								 int nParams,
+								 const char *const *paramValues,
+								 const int *paramLengths,
+								 const int *paramFormats,
+								 const int *paramForceColumnEncryptions,
+								 int resultFormat,
+								 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +472,15 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPrepared2(PGconn *conn,
+								 const char *stmtName,
+								 int nParams,
+								 const char *const *paramValues,
+								 const int *paramLengths,
+								 const int *paramFormats,
+								 const int *paramForceColumnEncryptions,
+								 int resultFormat,
+								 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +551,7 @@ extern int	PQfformat(const PGresult *res, int field_num);
 extern Oid	PQftype(const PGresult *res, int field_num);
 extern int	PQfsize(const PGresult *res, int field_num);
 extern int	PQfmod(const PGresult *res, int field_num);
+extern int	PQfisencrypted(const PGresult *res, int field_num);
 extern char *PQcmdStatus(PGresult *res);
 extern char *PQoidStatus(const PGresult *res);	/* old and ugly */
 extern Oid	PQoidValue(const PGresult *res);	/* new and improved */
@@ -540,6 +561,7 @@ extern int	PQgetlength(const PGresult *res, int tup_num, int field_num);
 extern int	PQgetisnull(const PGresult *res, int tup_num, int field_num);
 extern int	PQnparams(const PGresult *res);
 extern Oid	PQparamtype(const PGresult *res, int param_num);
+extern int	PQparamisencrypted(const PGresult *res, int param_num);
 
 /* Describe prepared statements and portals */
 extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 3db6a17db4..0a4c012aa6 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -111,6 +111,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -340,6 +343,26 @@ typedef struct pg_conn_host
 								 * found in password file. */
 } pg_conn_host;
 
+/*
+ * Column encryption support data
+ */
+
+/* column master key */
+typedef struct pg_cmk
+{
+	Oid			cmkid;
+	char	   *cmkname;
+	char	   *cmkrealm;
+} PGCMK;
+
+/* column encryption key */
+typedef struct pg_cek
+{
+	Oid			cekid;
+	unsigned char *cekdata;		/* (decrypted) */
+	size_t		cekdatalen;
+} PGCEK;
+
 /*
  * PGconn stores all the state data associated with a single connection
  * to a backend.
@@ -393,6 +416,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *cmklookup;		/* CMK lookup specification */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -475,6 +499,12 @@ struct pg_conn
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
 
+	/* Column encryption support data */
+	int			ncmks;
+	PGCMK	   *cmks;
+	int			nceks;
+	PGCEK	   *ceks;
+
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
 	int			inBufSize;		/* allocated size of buffer */
@@ -670,6 +700,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern int	pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+								  const char *keyrealm);
+extern int	pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg,
+									  const unsigned char *value, int len);
 extern int	pqRowProcessor(PGconn *conn, const char **errmsgp);
 extern void pqCommandQueueAdvance(PGconn *conn);
 extern int	PQsendQueryContinue(PGconn *conn, const char *query);
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 1f62ba1b57..844e085e6d 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
 AVAIL_LANGUAGES  = cs de el es fr he it ja ko pl pt_BR ru sv tr uk zh_CN zh_TW
-GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
+GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-encrypt-openssl.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
 GETTEXT_TRIGGERS = libpq_gettext pqInternalNotice:2
 GETTEXT_FLAGS    = libpq_gettext:1:pass-c-format pqInternalNotice:2:c-format
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 44457f930c..c5f3153094 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9529,7 +9529,7 @@ DO $d$
     END;
 $d$;
 ERROR:  invalid option "password"
-HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
+HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, cmklookup, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
 CONTEXT:  SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
 PL/pgSQL function inline_code_block line 3 at EXECUTE
 -- If we add a password for our user mapping instead, we should get a different

base-commit: 6ffff0fd225432fe2ae4bd5abb7ff6113e255418
-- 
2.36.1

#18Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#17)
1 attachment(s)
Re: Transparent column encryption

Updated patch, to resolve some merge conflicts.

Also, I added some CREATE DDL commands. These aren't fully robust yet,
but they do the basic job, so it makes the test cases easier to write
and read, and they can be referred to in the documentation. (Note that
the corresponding DROP aren't there yet.) I also expanded the
documentation in the DDL chapter to give a complete recipe of how to set
it up and use it.

Attachments:

v4-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v4-0001-Transparent-column-encryption.patchDownload
From 92de932c6e49ef29f55078767abe2fae1622a76a Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Tue, 12 Jul 2022 20:22:22 +0200
Subject: [PATCH v4] 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 the "prying eyes" of DBAs, sysadmins, cloud operators,
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.  Of course, you
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 you can do equality lookups, at the cost
of some security.

This functionality also exists in other SQL database products, so the
overall concepts weren't invented by me by any means.

Also, this feature has nothing to do with the on-disk encryption
feature being contemplated in parallel.  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.

TODO: Some protocol extensions are required.  These should be guarded
by some _pq_... setting, but this is not done in this patch yet.  As
mentioned above, extra messages are added for sending the CMKs and
CEKs.  In the RowDescription message, I have commandeered the format
field to add a bit that indicates that the field is encrypted.  This
could be made a separate field, and there should probably be
additional fields to indicate the algorithm and CEK name, but this was
easiest for now.  The ParameterDescription message is extended to
contain format fields for each parameter, for the same purpose.
Again, this could be done differently.

Speaking of parameter descriptions, 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:

- additional DDL support
- protocol versioning
- forward and backward compatibility
- pg_dump support
- 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                         | 310 +++++++
 doc/src/sgml/glossary.sgml                    |  23 +
 doc/src/sgml/libpq.sgml                       | 257 +++++-
 doc/src/sgml/protocol.sgml                    | 346 ++++++++
 doc/src/sgml/ref/allfiles.sgml                |   2 +
 .../ref/create_column_encryption_key.sgml     | 144 ++++
 .../sgml/ref/create_column_master_key.sgml    | 108 +++
 doc/src/sgml/ref/create_table.sgml            |  42 +-
 doc/src/sgml/ref/psql-ref.sgml                |  33 +
 doc/src/sgml/reference.sgml                   |   2 +
 src/test/Makefile                             |   3 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  29 +
 .../t/001_column_encryption.pl                | 180 +++++
 .../column_encryption/t/002_cmk_rotation.pl   | 118 +++
 src/test/column_encryption/test_client.c      | 210 +++++
 .../column_encryption/test_run_decrypt.pl     |  41 +
 .../traces/disallowed_in_pipeline.trace       |   2 +-
 .../traces/multi_pipelines.trace              |   4 +-
 .../libpq_pipeline/traces/nosync.trace        |  20 +-
 .../traces/pipeline_abort.trace               |   4 +-
 .../libpq_pipeline/traces/pipeline_idle.trace |  16 +-
 .../libpq_pipeline/traces/prepared.trace      |   6 +-
 .../traces/simple_pipeline.trace              |   2 +-
 .../libpq_pipeline/traces/singlerow.trace     |   6 +-
 .../libpq_pipeline/traces/transaction.trace   |   2 +-
 .../regress/expected/column_encryption.out    |  37 +
 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     |  20 +-
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    |  28 +
 src/test/regress/sql/prepare.sql              |   2 +-
 src/test/regress/sql/psql.sql                 |  17 +
 src/test/regress/sql/type_sanity.sql          |   4 +-
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |   9 +
 src/include/catalog/pg_cast.dat               |   6 +
 src/include/catalog/pg_colenckey.h            |  55 ++
 src/include/catalog/pg_colenckeydata.h        |  46 ++
 src/include/catalog/pg_colmasterkey.h         |  45 ++
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  10 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               |  13 +-
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  24 +
 src/include/nodes/parsenodes.h                |   3 +
 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                 |   2 +
 src/include/tcop/tcopprot.h                   |   2 +
 src/include/utils/plancache.h                 |   6 +
 src/include/utils/syscache.h                  |   5 +
 src/backend/access/common/printsimple.c       |   2 +
 src/backend/access/common/printtup.c          | 162 ++++
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |   4 +
 src/backend/catalog/heap.c                    |   3 +
 src/backend/catalog/objectaddress.c           |   2 +
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/colenccmds.c             | 245 ++++++
 src/backend/commands/event_trigger.c          |   6 +
 src/backend/commands/prepare.c                |  74 +-
 src/backend/commands/seclabel.c               |   2 +
 src/backend/commands/tablecmds.c              |  74 ++
 src/backend/executor/spi.c                    |   4 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/analyze.c                  |   3 +-
 src/backend/parser/gram.y                     |  39 +-
 src/backend/parser/parse_param.c              |  43 +-
 src/backend/parser/parse_target.c             |   6 +
 src/backend/tcop/postgres.c                   |  54 +-
 src/backend/tcop/utility.c                    |  15 +
 src/backend/utils/adt/arrayfuncs.c            |   1 +
 src/backend/utils/cache/plancache.c           |  10 +
 src/backend/utils/cache/syscache.c            |  48 ++
 src/bin/pg_dump/pg_dump.c                     |  26 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/command.c                        |  31 +
 src/bin/psql/common.c                         |  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                   |   2 +-
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  20 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 758 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 594 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 123 ++-
 src/interfaces/libpq/fe-trace.c               |   7 +
 src/interfaces/libpq/libpq-fe.h               |  22 +
 src/interfaces/libpq/libpq-int.h              |  34 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 .../postgres_fdw/expected/postgres_fdw.out    |   2 +-
 114 files changed, 5148 insertions(+), 140 deletions(-)
 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 src/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/t/002_cmk_rotation.pl
 create mode 100644 src/test/column_encryption/test_client.c
 create mode 100755 src/test/column_encryption/test_run_decrypt.pl
 create mode 100644 src/test/regress/expected/column_encryption.out
 create mode 100644 src/test/regress/sql/column_encryption.sql
 create mode 100644 src/include/catalog/pg_colenckey.h
 create mode 100644 src/include/catalog/pg_colenckeydata.h
 create mode 100644 src/include/catalog/pg_colmasterkey.h
 create mode 100644 src/include/commands/colenccmds.h
 create mode 100644 src/backend/commands/colenccmds.c
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h

diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 9ed148ab84..e21c9bcce3 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -56,6 +56,15 @@ <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 4f3f375a84..c5e30f8029 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>
@@ -2437,6 +2486,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 8e30b82273..cdd0530eeb 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5342,4 +5342,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 b01e3ad544..607b4cd790 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1211,6 +1211,316 @@ <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>
+    </listitem>
+   </orderedlist>
+  </para>
+  </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 d6d0a3a814..10507e4391 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 74456aa69d..310aa49e37 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1974,6 +1974,110 @@ <title>Parameter Key Words</title>
        </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>
@@ -3022,6 +3126,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>
 
@@ -3872,6 +4027,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>
 
@@ -4047,12 +4224,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>
 
@@ -4578,6 +4780,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
@@ -4585,6 +4788,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.
@@ -4696,6 +4900,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>
 
@@ -4746,6 +4990,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"/>
@@ -7776,6 +8021,16 @@ <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>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index c0b89a3c01..60595c33b1 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1050,6 +1050,52 @@ <title>Extended Query</title>
    </note>
   </sect2>
 
+  <sect2>
+   <title>Transparent Column Encryption</title>
+
+   <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>
+    The cryptographic operations used for transparent column encryption are
+    described in <xref linkend="protocol-transparent-column-encryption"/>.
+   </para>
+  </sect2>
+
   <sect2>
    <title>Function Call</title>
 
@@ -3991,6 +4037,128 @@ <title>Message Formats</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ColumnEncryptionKey">
+    <term>ColumnEncryptionKey (B)</term>
+    <listitem>
+     <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>
+     <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>
@@ -5086,6 +5254,37 @@ <title>Message Formats</title>
         </para>
        </listitem>
       </varlistentry>
+
+      <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>
@@ -5474,6 +5673,26 @@ <title>Message Formats</title>
         </para>
        </listitem>
       </varlistentry>
+
+      <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 encrypt algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
      </variablelist>
     </listitem>
    </varlistentry>
@@ -7278,6 +7497,133 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-transparent-column-encryption">
+  <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 e90a0e1f83..9ae56ee950 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -62,6 +62,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">
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 0000000000..e7965ebcca
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,144 @@
+<!--
+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>
+ </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>
+ </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">
+  <!-- TODO
+   <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 0000000000..db3043d715
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,108 @@
+<!--
+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">
+  <!-- TODO
+   <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 6c9918b0a1..7a1d03f868 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> [ 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> [ COMPRESSION <replaceable>compression_method</replaceable> ] [ ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> ) ] [ COLLATE <replaceable>collation</replaceable> ] [ <replaceable class="parameter">column_constraint</replaceable> [ ... ] ]
     | <replaceable>table_constraint</replaceable>
     | LIKE <replaceable>source_table</replaceable> [ <replaceable>like_option</replaceable> ... ] }
     [, ... ]
@@ -322,6 +322,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/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 65bb0a6a3f..e6c93f1b13 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2255,6 +2255,28 @@ <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>
 
@@ -3945,6 +3967,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 a3b743e8c1..2e37500031 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -90,6 +90,8 @@ <title>SQL Commands</title>
    &createAggregate;
    &createCast;
    &createCollation;
+   &createColumnEncryptionKey;
+   &createColumnMasterKey;
    &createConversion;
    &createDatabase;
    &createDomain;
diff --git a/src/test/Makefile b/src/test/Makefile
index 69ef074d75..2300be8332 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -32,6 +32,7 @@ SUBDIRS += ldap
 endif
 endif
 ifeq ($(with_ssl),openssl)
+SUBDIRS += column_encryption
 ifneq (,$(filter ssl,$(PG_TEST_EXTRA)))
 SUBDIRS += ssl
 endif
@@ -41,7 +42,7 @@ endif
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl column_encryption)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..7aca84b17a
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,29 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/column_encryption/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..a0d3192800
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,180 @@
+# 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{'PGCMKLOOKUP'} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl3 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c text ENCRYPTED WITH (column_encryption_key = cek2, algorithm = 'AEAD_AES_192_CBC_HMAC_SHA_384')
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b) VALUES (1, $1) \gencr 'val1'
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \gencr 'val2'
+});
+
+# Expected ciphertext length is 2 blocks of AES output (2 * 16) plus
+# half SHA-256 output (16) in hex encoding: (2 * 16 + 16) * 2 = 96
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1) TO STDOUT}),
+	qr/1\t\\\\x[0-9a-f]{96}\n2\t\\\\x[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'decrypted query result');
+
+{
+	local $ENV{'PGCMKLOOKUP'} = '*=run:broken %k "%b"';
+	$result = $node->psql('postgres', q{SELECT a, b FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{'PGCMKLOOKUP'} = "*=run:perl ./test_run_decrypt.pl '${PostgreSQL::Test::Utils::tmp_check}' %k %a '%b'";
+
+	$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+	is($result,
+		q(1|val1
+2|val2),
+		'decrypted query result with cmklookup run');
+}
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES (1, $1, $2) \gencr 'valB1' 'valC1'
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+
+$node->command_fails_like(['test_client', 'test1'], qr/not encrypted/, 'test client fails because parameters not encrypted');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'decrypted query result after test client insert');
+
+$node->command_ok(['test_client', 'test2'], 'test client test 2');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3),
+	'decrypted query result after test client insert 2');
+
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1 WHERE a = 3) TO STDOUT}),
+	qr/3\t\\\\x[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+$node->command_ok(['test_client', 'test3'], 'test client test 3');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3upd),
+	'decrypted query result after test client insert 3');
+
+$node->command_ok(['test_client', 'test4'], 'test client test 4');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result after test client insert 4');
+
+is($node->safe_psql('postgres', q{SELECT b, count(*) FROM tbl2 GROUP BY b ORDER BY 2}),
+	q(valB|1
+valA|2),
+	'group by deterministically encrypted column');
+
+$node->command_ok(['test_client', 'test5'], 'test client test 5');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1
+2|valB2|valC2
+3|valB3|valC3),
+	'decrypted query result multiple keys after test client insert 5');
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..cf6114db46
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,118 @@
+# 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{'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
+# TODO: There should be some ALTER COLUMN ENCRYPTION KEY command to do this.
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = '${cekname}'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk2'),
+    1,
+    '\\x${cekenchex}'
+);
+});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with two CMKs');
+
+
+# delete CEK record for first CMK
+$node->safe_psql('postgres', qq{
+DELETE FROM pg_colenckeydata WHERE ckdcmkid = (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1');
+});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with only new CMK');
+
+
+done_testing();
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..add9fe40a6
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2021-2022, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3"};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					   2, NULL, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3"};
+	int			forces[] = {false, true};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					2, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (!(!PQparamisencrypted(res2, 0) &&
+		  PQparamisencrypted(res2, 1)))
+	{
+		fprintf(stderr, "wrong results from PQparamisencrypted()\n");
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, forces, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3upd"};
+
+	res = PQprepare(conn, "", "UPDATE tbl1 SET b = $2 WHERE a = $1",
+					2, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"1", "valA", "2", "valB", "3", "valA"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6)",
+					6, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 6, values, NULL, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test5(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {
+		"2", "valB2", "valC2",
+		"3", "valB3", "valC3"
+	};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6)",
+					6, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 6, values, NULL, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	PGconn	   *conn;
+	int			ret = 0;
+
+	conn = PQconnectdb("");
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		fprintf(stderr, "Connection to database failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (argc < 2 || argv[1] == NULL)
+		return 87;
+	else if (strcmp(argv[1], "test1") == 0)
+		ret = test1(conn);
+	else if (strcmp(argv[1], "test2") == 0)
+		ret = test2(conn);
+	else if (strcmp(argv[1], "test3") == 0)
+		ret = test3(conn);
+	else if (strcmp(argv[1], "test4") == 0)
+		ret = test4(conn);
+	else if (strcmp(argv[1], "test5") == 0)
+		ret = test5(conn);
+	else
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..9664349f14
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,41 @@
+#!/usr/bin/perl
+
+# Test/sample command for libpq cmklookup run scheme
+#
+# This just places the data into temporary files and runs the openssl
+# command on it.  (In practice, this could more simply be written as a
+# shell script, but this way it's more portable.)
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use MIME::Base64;
+
+my ($tmpdir, $cmkname, $alg, $b64data) = @ARGV;
+
+die unless $alg eq 'RSAES_OAEP_SHA_1';
+
+open my $fh, '>:raw', "${tmpdir}/input.tmp" or die;
+print $fh decode_base64($b64data);
+close $fh;
+
+system('openssl', 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', "${tmpdir}/input.tmp", '-out', "${tmpdir}/output.tmp") == 0 or die;
+
+open $fh, '<:raw', "${tmpdir}/output.tmp" or die;
+my $data = '';
+
+while (1) {
+	my $success = read $fh, $data, 100, length($data);
+	die $! if not defined $success;
+	last if not $success;
+}
+
+close $fh;
+
+unlink "${tmpdir}/input.tmp", "${tmpdir}/output.tmp";
+
+print encode_base64($data);
diff --git a/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace b/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace
index dd6df03f1e..5fc9c0346d 100644
--- a/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace
+++ b/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace
@@ -1,5 +1,5 @@
 F	13	Query	 "SELECT 1"
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace b/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace
index 4b9ab07ca4..700b0b6519 100644
--- a/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace
+++ b/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace
@@ -10,13 +10,13 @@ F	9	Execute	 "" 0
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/nosync.trace b/src/test/modules/libpq_pipeline/traces/nosync.trace
index d99aac649d..7b1dfa1c15 100644
--- a/src/test/modules/libpq_pipeline/traces/nosync.trace
+++ b/src/test/modules/libpq_pipeline/traces/nosync.trace
@@ -41,52 +41,52 @@ F	9	Execute	 "" 0
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 F	4	Terminate
diff --git a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
index 3fce548b99..6e4309cadd 100644
--- a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
+++ b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
@@ -50,14 +50,14 @@ F	6	Close	 P ""
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 65535 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	32	DataRow	 1 22 '0.33333333333333333333'
 B	32	DataRow	 1 22 '0.50000000000000000000'
 B	32	DataRow	 1 22 '1.00000000000000000000'
 B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "22012" M "division by zero" F "SSSS" L "SSSS" R "SSSS" \x00
 B	5	ReadyForQuery	 I
 F	40	Query	 "SELECT itemno FROM pq_pipeline_demo"
-B	31	RowDescription	 1 "itemno" NNNN 2 NNNN 4 -1 0
+B	37	RowDescription	 1 "itemno" NNNN 2 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '3'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/pipeline_idle.trace b/src/test/modules/libpq_pipeline/traces/pipeline_idle.trace
index 3957ee4dfe..d937d1b8f3 100644
--- a/src/test/modules/libpq_pipeline/traces/pipeline_idle.trace
+++ b/src/test/modules/libpq_pipeline/traces/pipeline_idle.trace
@@ -6,7 +6,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
@@ -20,12 +20,12 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
 F	13	Query	 "SELECT 2"
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '2'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
@@ -37,7 +37,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
@@ -49,7 +49,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '2'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
@@ -61,7 +61,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
@@ -73,7 +73,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '2'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
@@ -85,7 +85,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	43	RowDescription	 1 "pg_advisory_unlock" NNNN 0 NNNN 1 -1 0
+B	49	RowDescription	 1 "pg_advisory_unlock" NNNN 0 NNNN 1 -1 0 NNNN 0
 B	NN	NoticeResponse	 S "WARNING" V "WARNING" C "01000" M "you don't own a lock of type ExclusiveLock" F "SSSS" L "SSSS" R "SSSS" \x00
 B	11	DataRow	 1 1 'f'
 B	13	CommandComplete	 "SELECT 1"
diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace
index 1a7de5c3e6..8d45bbc0ce 100644
--- a/src/test/modules/libpq_pipeline/traces/prepared.trace
+++ b/src/test/modules/libpq_pipeline/traces/prepared.trace
@@ -2,8 +2,8 @@ F	68	Parse	 "select_one" "SELECT $1, '42', $1::numeric, interval '1 sec'" 1 NNNN
 F	16	Describe	 S "select_one"
 F	4	Sync
 B	4	ParseComplete
-B	10	ParameterDescription	 1 NNNN
-B	113	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 65535 -1 0 "numeric" NNNN 0 NNNN 65535 -1 0 "interval" NNNN 0 NNNN 16 -1 0
+B	18	ParameterDescription	 1 NNNN NNNN 0 0
+B	137	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0 "?column?" NNNN 0 NNNN 65535 -1 0 NNNN 0 "numeric" NNNN 0 NNNN 65535 -1 0 NNNN 0 "interval" NNNN 0 NNNN 16 -1 0 NNNN 0
 B	5	ReadyForQuery	 I
 F	10	Query	 "BEGIN"
 B	10	CommandComplete	 "BEGIN"
@@ -13,6 +13,6 @@ B	19	CommandComplete	 "DECLARE CURSOR"
 B	5	ReadyForQuery	 T
 F	16	Describe	 P "cursor_one"
 F	4	Sync
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	5	ReadyForQuery	 T
 F	4	Terminate
diff --git a/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace b/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace
index 5c94749bc1..5f4849425d 100644
--- a/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace
+++ b/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace
@@ -5,7 +5,7 @@ F	9	Execute	 "" 0
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/singlerow.trace b/src/test/modules/libpq_pipeline/traces/singlerow.trace
index 9de99befcc..199df4d4f4 100644
--- a/src/test/modules/libpq_pipeline/traces/singlerow.trace
+++ b/src/test/modules/libpq_pipeline/traces/singlerow.trace
@@ -13,14 +13,14 @@ F	9	Execute	 "" 0
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	40	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0
+B	46	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	12	DataRow	 1 2 '42'
 B	12	DataRow	 1 2 '43'
 B	12	DataRow	 1 2 '44'
 B	13	CommandComplete	 "SELECT 3"
 B	4	ParseComplete
 B	4	BindComplete
-B	40	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0
+B	46	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	12	DataRow	 1 2 '42'
 B	12	DataRow	 1 2 '43'
 B	12	DataRow	 1 2 '44'
@@ -28,7 +28,7 @@ B	12	DataRow	 1 2 '45'
 B	13	CommandComplete	 "SELECT 4"
 B	4	ParseComplete
 B	4	BindComplete
-B	40	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0
+B	46	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	12	DataRow	 1 2 '42'
 B	12	DataRow	 1 2 '43'
 B	12	DataRow	 1 2 '44'
diff --git a/src/test/modules/libpq_pipeline/traces/transaction.trace b/src/test/modules/libpq_pipeline/traces/transaction.trace
index 1dcc2373c0..a6869c4a5b 100644
--- a/src/test/modules/libpq_pipeline/traces/transaction.trace
+++ b/src/test/modules/libpq_pipeline/traces/transaction.trace
@@ -54,7 +54,7 @@ B	15	CommandComplete	 "INSERT 0 1"
 B	5	ReadyForQuery	 I
 B	5	ReadyForQuery	 I
 F	34	Query	 "SELECT * FROM pq_pipeline_tst"
-B	27	RowDescription	 1 "id" NNNN 1 NNNN 4 -1 0
+B	33	RowDescription	 1 "id" NNNN 1 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '3'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..7f78dde6e1
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,37 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+CREATE COLUMN ENCRYPTION KEY cek1 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    |           |          |         | extended | cek1       |              | 
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..2aa0e16323 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attrealtypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,7 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 86d755aa44..4a366074da 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -175,13 +175,14 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+(7 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +198,13 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
  anyrange                    | anymultirange
-(5 rows)
+(6 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -841,6 +843,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+pg_encrypted_det_eq(pg_encrypted_det,pg_encrypted_det)
+pg_encrypted_det_ne(pg_encrypted_det,pg_encrypted_det)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
@@ -988,7 +992,9 @@ WHERE c.castmethod = 'b' AND
  xml               | text              |        0 | a
  xml               | character varying |        0 | a
  xml               | character         |        0 | a
-(10 rows)
+ bytea             | pg_encrypted_det  |        0 | e
+ bytea             | pg_encrypted_rnd  |        0 | e
+(12 rows)
 
 -- **************** pg_conversion ****************
 -- Look for illegal values in pg_conversion fields.
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39..4482a65d24 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 60acbd1241..33956f6993 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -237,6 +237,31 @@ SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g
  3 | Hello    | 4 | t
 (1 row)
 
+-- \gencr
+-- (This just tests the parameter passing; there is no encryption here.)
+CREATE TABLE test_gencr (a int, b text);
+INSERT INTO test_gencr VALUES (1, 'one') \gencr
+SELECT * FROM test_gencr WHERE a = 1 \gencr
+ a |  b  
+---+-----
+ 1 | one
+(1 row)
+
+INSERT INTO test_gencr VALUES ($1, $2) \gencr 2 'two'
+SELECT * FROM test_gencr WHERE a IN ($1, $2) \gencr 2 3
+ a |  b  
+---+-----
+ 2 | two
+(1 row)
+
+-- test parse error
+SELECT * FROM test_gencr WHERE a = x \gencr
+ERROR:  column "x" does not exist
+LINE 1: SELECT * FROM test_gencr WHERE a = x 
+                                           ^
+-- test bind error
+SELECT * FROM test_gencr WHERE a = $1 \gencr
+ERROR:  bind message supplies 0 parameters, but prepared statement "" requires 1
 -- \gexec
 create temporary table gexec_test(a int, b text, c date, d float);
 select format('create index on gexec_test(%I)', attname)
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 7ec3d2688f..42e38d66d1 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 d3ac08c9ee..fc0c5896e4 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -17,7 +17,7 @@ SELECT t1.oid, t1.typname
 FROM pg_type as t1
 WHERE t1.typnamespace = 0 OR
     (t1.typlen <= 0 AND t1.typlen != -1 AND t1.typlen != -2) OR
-    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r')) OR
+    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT t1.typisdefined OR
     (t1.typalign not in ('c', 's', 'i', 'd')) OR
     (t1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -75,7 +75,9 @@ ORDER BY t1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | pg_encrypted_det
+ 8244 | pg_encrypted_rnd
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT t1.oid, t1.typname as basetype, t2.typname as arraytype,
@@ -210,7 +212,8 @@ ORDER BY 1;
  e       | enum_in
  m       | multirange_in
  r       | range_in
-(5 rows)
+ y       | byteain
+(6 rows)
 
 -- Check for bogus typoutput routines
 -- As of 8.0, this check finds refcursor, which is borrowing
@@ -255,7 +258,8 @@ ORDER BY 1;
  e       | enum_out
  m       | multirange_out
  r       | range_out
-(4 rows)
+ y       | byteaout
+(5 rows)
 
 -- Domains should have same typoutput as their base types
 SELECT t1.oid, t1.typname, t2.oid, t2.typname
@@ -335,7 +339,8 @@ ORDER BY 1;
  e       | enum_recv
  m       | multirange_recv
  r       | range_recv
-(5 rows)
+ y       | bytearecv
+(6 rows)
 
 -- Check for bogus typsend routines
 -- As of 7.4, this check finds refcursor, which is borrowing
@@ -380,7 +385,8 @@ ORDER BY 1;
  e       | enum_send
  m       | multirange_send
  r       | range_send
-(4 rows)
+ y       | byteasend
+(5 rows)
 
 -- Domains should have same typsend as their base types
 SELECT t1.oid, t1.typname, t2.oid, t2.typname
@@ -707,6 +713,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::pg_encrypted_rnd,
+  E'\\xDEADBEEF'::pg_encrypted_det,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 103e11483d..ffca206c6f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -129,6 +129,9 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # ----------
 test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats
 
+# WIP
+test: column_encryption
+
 # event_trigger cannot run concurrently with any test that runs DDL
 # oidjoins is read-only, though, and should run late for best coverage
 test: event_trigger oidjoins
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index a4b354c9e6..8ad1f458d5 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..0ca1d56210
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,28 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek1 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
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95c..7db788735c 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 1149c6a839..e88c70d302 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -119,6 +119,23 @@ CREATE TABLE bububu(a int) \gdesc
 -- all on one line
 SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g
 
+
+-- \gencr
+-- (This just tests the parameter passing; there is no encryption here.)
+
+CREATE TABLE test_gencr (a int, b text);
+INSERT INTO test_gencr VALUES (1, 'one') \gencr
+SELECT * FROM test_gencr WHERE a = 1 \gencr
+
+INSERT INTO test_gencr VALUES ($1, $2) \gencr 2 'two'
+SELECT * FROM test_gencr WHERE a IN ($1, $2) \gencr 2 3
+
+-- test parse error
+SELECT * FROM test_gencr WHERE a = x \gencr
+-- test bind error
+SELECT * FROM test_gencr WHERE a = $1 \gencr
+
+
 -- \gexec
 
 create temporary table gexec_test(a int, b text, c date, d float);
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 5edc1f1f6e..34dd19456d 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -20,7 +20,7 @@
 FROM pg_type as t1
 WHERE t1.typnamespace = 0 OR
     (t1.typlen <= 0 AND t1.typlen != -1 AND t1.typlen != -2) OR
-    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r')) OR
+    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT t1.typisdefined OR
     (t1.typalign not in ('c', 's', 'i', 'd')) OR
     (t1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -529,6 +529,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::pg_encrypted_rnd,
+  E'\\xDEADBEEF'::pg_encrypted_det,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 971a74cf22..db1f3b8811 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,6 +20,8 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 61cd591430..a0bd708132 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4cc129bebd..dddb27113f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 053294c99f..550a53649b 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,15 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	bool		attislocal BKI_DEFAULT(t);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey);
+
+	/* real type if encrypted */
+	Oid			attrealtypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+
+	/* encryption algorithm (PG_CEK_* values) */
+	int16		attencalg BKI_DEFAULT(0);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index 4471eb6bbe..878b0bcbbf 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -546,4 +546,10 @@
 { castsource => 'tstzrange', casttarget => 'tstzmultirange',
   castfunc => 'tstzmultirange(tstzrange)', castcontext => 'e',
   castmethod => 'f' },
+
+{ castsource => 'bytea', casttarget => 'pg_encrypted_det', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+{ castsource => 'bytea', casttarget => 'pg_encrypted_rnd', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+
 ]
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..095ed712b0
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,55 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+/* ----------------
+ *		pg_colenckey definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckey
+ * ----------------
+ */
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_index, 8242, ColumnEncKeyNameIndexId, on pg_colenckey using btree(cekname name_ops));
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  For clarity, the assigned numbers are not reused between CMKs
+ * and CEKs, but that is not technically required.  In either case, don't
+ * assign zero, so that that can be used as an invalid value.
+ */
+
+#define PG_CMK_RSAES_OAEP_SHA_1			1
+#define PG_CMK_RSAES_OAEP_SHA_256		2
+
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	130
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	131
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	132
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	133
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..3e4dea7218
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkeydata.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+/* ----------------
+ *		pg_colenckeydata definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckeydata
+ * ----------------
+ */
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+	int16		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..0344cc4201
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,45 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_index, 8241, ColumnMasterKeyNameIndexId, on pg_colmasterkey using btree(cmkname name_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index dbcae7ffdd..0ca401ffe4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index bc5f8213f3..4737a7f9ed 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index b3b6a7e616..5343580b06 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2e41f4d9e8..2fa26a2222 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8025,9 +8025,9 @@
   proname => 'pg_prepared_statement', prorows => '1000', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'record',
   proargtypes => '',
-  proallargtypes => '{text,text,timestamptz,_regtype,_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',
@@ -11886,4 +11886,11 @@
   prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary',
   prosrc => 'brin_minmax_multi_summary_send' },
 
+{ oid => '8245',
+  proname => 'pg_encrypted_det_eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'pg_encrypted_det_ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index df45879463..df1fa3e393 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,16 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 48a2559137..11aff9ff83 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -277,6 +277,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPTYPE_MULTIRANGE	'm' /* multirange type */
 #define  TYPTYPE_PSEUDO		'p' /* pseudo-type */
 #define  TYPTYPE_RANGE		'r' /* range type */
+#define  TYPTYPE_ENCRYPTED	'y' /* encrypted column value */
 
 #define  TYPCATEGORY_INVALID	'\0'	/* not an allowed category */
 #define  TYPCATEGORY_ARRAY		'A'
diff --git a/src/include/commands/colenccmds.h b/src/include/commands/colenccmds.h
new file mode 100644
index 0000000000..1019182925
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,24 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 CreateCMK(ParseState *pstate, DefineStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0b6a7bb365..8b8edddace 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -690,6 +690,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? */
@@ -2154,6 +2155,8 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index dc379547c7..c310e124f1 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -28,7 +28,9 @@ extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook;
 extern Query *parse_analyze_fixedparams(RawStmt *parseTree, const char *sourceText,
 										const Oid *paramTypes, int numParams, QueryEnvironment *queryEnv);
 extern Query *parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
-									  Oid **paramTypes, int *numParams, QueryEnvironment *queryEnv);
+									  Oid **paramTypes, int *numParams,
+									  Oid **paramOrigTbls, AttrNumber **paramOrigCols,
+									  QueryEnvironment *queryEnv);
 extern Query *parse_analyze_withcb(RawStmt *parseTree, const char *sourceText,
 								   ParserSetupHook parserSetup,
 								   void *parserSetupArg,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index ae35f03251..13ccadfd5d 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -152,6 +152,7 @@ PG_KEYWORD("empty", EMPTY_P, UNRESERVED_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("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -268,6 +269,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 cf9c759025..225a2a8733 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -93,6 +93,7 @@ typedef Node *(*ParseParamRefHook) (ParseState *pstate, ParamRef *pref);
 typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param,
 								  Oid targetTypeId, int32 targetTypeMod,
 								  int location);
+typedef void (*ParamAssignOrigHook) (ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 
 
 /*
@@ -222,6 +223,7 @@ struct ParseState
 	PostParseColumnRefHook p_post_columnref_hook;
 	ParseParamRefHook p_paramref_hook;
 	CoerceParamHook p_coerce_param_hook;
+	ParamAssignOrigHook p_param_assign_orig_hook;
 	void	   *p_ref_hook_state;	/* common passthrough link for above */
 };
 
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index df1ee660d8..da202b28a7 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -18,7 +18,8 @@
 extern void setup_parse_fixed_parameters(ParseState *pstate,
 										 const Oid *paramTypes, int numParams);
 extern void setup_parse_variable_parameters(ParseState *pstate,
-											Oid **paramTypes, int *numParams);
+											Oid **paramTypes, int *numParams,
+											Oid **paramOrigTbls, AttrNumber **paramOrigCols);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 2b1163ce33..70c2cfd05b 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -86,6 +86,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)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 70d9dab25b..3038ceb522 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -53,6 +53,8 @@ extern List *pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 											  const char *query_string,
 											  Oid **paramTypes,
 											  int *numParams,
+											  Oid **paramOrigTbls,
+											  AttrNumber **paramOrigCols,
 											  QueryEnvironment *queryEnv);
 extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree,
 										   const char *query_string,
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 0499635f59..9a7cf794ce 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 4463ea66be..d0b81766c7 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,13 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATAOID,
+	CEKNAME,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAME,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index c99ae54cb0..9d3476814f 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -46,6 +46,8 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc)
 		pq_sendint16(&buf, attr->attlen);
 		pq_sendint32(&buf, attr->atttypmod);
 		pq_sendint16(&buf, 0);	/* format code */
+		pq_sendint32(&buf, 0);	/* CEK */
+		pq_sendint16(&buf, 0);	/* CEK alg */
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index d2f3b57288..28b437e5c7 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,26 @@
  */
 #include "postgres.h"
 
+#include "access/genam.h"
 #include "access/printtup.h"
+#include "access/skey.h"
+#include "access/table.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/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 +164,135 @@ 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;
+
+	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;
+
+	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
  *
@@ -200,6 +342,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 +375,22 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (get_typtype(atttypid) == TYPTYPE_ENCRYPTED)
+		{
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(resorigtbl), Int16GetDatum(resorigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", resorigcol, resorigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			atttypid = orig_att->attrealtypid;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->attencalg;
+			ReleaseSysCache(tp);
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +398,8 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		pq_writeint16(buf, att->attlen);
 		pq_writeint32(buf, atttypmod);
 		pq_writeint16(buf, format);
+		pq_writeint32(buf, attcekid);
+		pq_writeint16(buf, attencalg);
 	}
 
 	pq_endmessage_reuse(buf);
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index d6fb261e20..e3ecc64ea7 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attrealtypid != attr2->attrealtypid)
+			return false;
+		if (attr1->attencalg != attr2->attencalg)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 10bf26ce7c..cea91d4a88 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 89a0221ec9..7e1a91b974 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 5f1726c095..61b58c64a2 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3631,6 +3631,8 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEK:
+					case OBJECT_CMK:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3771,6 +3773,8 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEK:
+					case OBJECT_CMK:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index e770ea6eb8..9c651d87b4 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -746,6 +746,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attrealtypid - 1] = ObjectIdGetDatum(attrs->attrealtypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attencalg - 1] = Int16GetDatum(attrs->attencalg);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2d21db4690..4eff111fa1 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2303,6 +2303,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:
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91..69f6175c60 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/colenccmds.c b/src/backend/commands/colenccmds.c
new file mode 100644
index 0000000000..205733bdfc
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,245 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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/rel.h"
+#include "utils/syscache.h"
+
+ObjectAddress
+CreateCEK(ParseState *pstate, DefineStmt *stmt)
+{
+	AclResult	aclresult;
+	Relation	rel;
+	Relation	rel2;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = 0;
+	int16		alg;
+	char	   *encval;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	Datum		values2[Natts_pg_colenckeydata] = {0};
+	bool		nulls2[Natts_pg_colenckeydata] = {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);
+	rel2 = table_open(ColumnEncKeyDataRelationId, 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)))));
+
+	foreach(lc, stmt->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 (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)
+		encval = defGetString(encvalEl);
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("attribute \"%s\" must be specified",
+						"encrypted_value")));
+
+	/* pg_colenckey */
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+	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);
+
+	/* pg_colenckeydata */
+	values2[Anum_pg_colenckeydata_oid - 1] =
+		ObjectIdGetDatum(GetNewOidWithIndex(rel, ColumnEncKeyDataOidIndexId, Anum_pg_colenckeydata_oid));
+	values2[Anum_pg_colenckeydata_ckdcekid - 1] = ObjectIdGetDatum(cekoid);
+	values2[Anum_pg_colenckeydata_ckdcmkid - 1] = ObjectIdGetDatum(cmkoid);
+	values2[Anum_pg_colenckeydata_ckdcmkalg - 1] = Int16GetDatum(alg);
+	values2[Anum_pg_colenckeydata_ckdencval - 1] = DirectFunctionCall1(byteain, CStringGetDatum(encval));
+
+	tup = heap_form_tuple(RelationGetDescr(rel2), values2, nulls2);
+	CatalogTupleInsert(rel2, tup);
+	heap_freetuple(tup);
+
+	recordDependencyOnOwner(ColumnEncKeyRelationId, cekoid, GetUserId());
+
+	ObjectAddressSet(myself, ColumnEncKeyRelationId, cekoid);
+
+	table_close(rel2, RowExclusiveLock);
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnMasterKeyRelationId, cekoid, 0);
+
+	return myself;
+}
+
+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 f46f86474a..93ac34c83d 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,8 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -2060,6 +2062,8 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2143,6 +2147,8 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 2333aae467..94578350bd 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -50,7 +50,6 @@ static void InitQueryHashTable(void);
 static ParamListInfo EvaluateParams(ParseState *pstate,
 									PreparedStatement *pstmt, List *params,
 									EState *estate);
-static Datum build_regtype_array(Oid *param_types, int num_params);
 
 /*
  * Implements the 'PREPARE' utility statement.
@@ -62,6 +61,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	RawStmt    *rawstmt;
 	CachedPlanSource *plansource;
 	Oid		   *argtypes = NULL;
+	Oid		   *argorigtbls = NULL;
+	AttrNumber *argorigcols = NULL;
 	int			nargs;
 	List	   *query_list;
 
@@ -108,6 +109,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 
 			argtypes[i++] = toid;
 		}
+
+		argorigtbls = (Oid *) palloc0(nargs * sizeof(Oid));
+		argorigcols = (AttrNumber *) palloc0(nargs * sizeof(AttrNumber));
 	}
 
 	/*
@@ -117,7 +121,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
 	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
-												  &argtypes, &nargs, NULL);
+												  &argtypes, &nargs,
+												  &argorigtbls, &argorigcols,
+												  NULL);
 
 	/* Finish filling in the CachedPlanSource */
 	CompleteCachedPlan(plansource,
@@ -125,6 +131,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 					   NULL,
 					   argtypes,
 					   nargs,
+					   argorigtbls,
+					   argorigcols,
 					   NULL,
 					   NULL,
 					   CURSOR_OPT_PARALLEL_OK,	/* allow parallel mode */
@@ -683,9 +691,11 @@ 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];
+			Datum	   *tmp_ary;
+			Datum		values[10];
+			bool		nulls[10];
 
 			result_desc = prep_stmt->plansource->resultDesc;
 
@@ -694,25 +704,38 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 			values[0] = CStringGetTextDatum(prep_stmt->stmt_name);
 			values[1] = CStringGetTextDatum(prep_stmt->plansource->query_string);
 			values[2] = TimestampTzGetDatum(prep_stmt->prepare_time);
-			values[3] = build_regtype_array(prep_stmt->plansource->param_types,
-											prep_stmt->plansource->num_params);
+
+			tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_types[i]);
+			values[3] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGTYPEOID));
+
+			tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_origtbls[i]);
+			values[4] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGCLASSOID));
+
+			tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = Int16GetDatum(prep_stmt->plansource->param_origcols[i]);
+			values[5] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, INT2OID));
+
 			if (result_desc)
 			{
-				Oid		   *result_types;
-
-				result_types = (Oid *) palloc(result_desc->natts * sizeof(Oid));
+				tmp_ary = (Datum *) palloc(result_desc->natts * sizeof(Datum));
 				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);
@@ -721,24 +744,3 @@ pg_prepared_statement(PG_FUNCTION_ARGS)
 
 	return (Datum) 0;
 }
-
-/*
- * This utility function takes a C array of Oids, and returns a Datum
- * pointing to a one-dimensional Postgres array of regtypes. An empty
- * array is returned as a zero-element array, not NULL.
- */
-static Datum
-build_regtype_array(Oid *param_types, int num_params)
-{
-	Datum	   *tmp_ary;
-	ArrayType  *result;
-	int			i;
-
-	tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
-
-	for (i = 0; i < num_params; i++)
-		tmp_ary[i] = ObjectIdGetDatum(param_types[i]);
-
-	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 7ae19b98bb..9ccdf7616c 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,8 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		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 ef5b34a312..3ab2798b9f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -35,6 +35,7 @@
 #include "catalog/partition.h"
 #include "catalog/pg_am.h"
 #include "catalog/pg_attrdef.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_depend.h"
@@ -931,6 +932,76 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 		if (colDef->compression)
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
+
+		if (colDef->encryption)
+		{
+			ListCell   *lc;
+			char	   *cek = NULL;
+			Oid			cekoid;
+			bool		encdet = false;
+			int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+			foreach(lc, colDef->encryption)
+			{
+				DefElem    *el = lfirst_node(DefElem, lc);
+
+				if (strcmp(el->defname, "column_encryption_key") == 0)
+					cek = strVal(linitial(castNode(TypeName, el->arg)->names));
+				else if (strcmp(el->defname, "encryption_type") == 0)
+				{
+					char	   *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+					if (strcmp(val, "deterministic") == 0)
+						encdet = true;
+					else if (strcmp(val, "randomized") == 0)
+						encdet = false;
+					else
+						ereport(ERROR,
+								errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								errmsg("unrecognized encryption type: %s", val));
+				}
+				else if (strcmp(el->defname, "algorithm") == 0)
+				{
+					char	   *val = strVal(el->arg);
+
+					if (strcmp(val, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+						alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+					else if (strcmp(val, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+						alg = PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+					else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+						alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+					else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+						alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+					else
+						ereport(ERROR,
+								errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								errmsg("unrecognized encryption algorithm: %s", val));
+				}
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("unrecognized column encryption parameter: %s", el->defname));
+			}
+
+			if (!cek)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+						errmsg("column encryption key must be specified"));
+
+			cekoid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid, PointerGetDatum(cek));
+			if (!cekoid)
+				ereport(ERROR,
+						errcode(ERRCODE_UNDEFINED_OBJECT),
+						errmsg("column encryption key \"%s\" does not exist", cek));
+
+			attr->attcek = cekoid;
+			attr->attrealtypid = attr->atttypid;
+			if (encdet)
+				attr->atttypid = PG_ENCRYPTED_DETOID;
+			else
+				attr->atttypid = PG_ENCRYPTED_RNDOID;
+			attr->attencalg = alg;
+		}
 	}
 
 	/*
@@ -6830,6 +6901,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attgenerated = colDef->generated;
 	attribute.attisdropped = false;
 	attribute.attislocal = colDef->is_local;
+	attribute.attcek = 0; // TODO
+	attribute.attrealtypid = 0; // TODO
+	attribute.attencalg = 0; // TODO
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
 
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 29bc26669b..cde5b0e087 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2279,6 +2279,8 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan)
 						   NULL,
 						   plan->argtypes,
 						   plan->nargs,
+						   NULL,
+						   NULL,
 						   plan->parserSetup,
 						   plan->parserSetupArg,
 						   plan->cursor_options,
@@ -2516,6 +2518,8 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 							   NULL,
 							   plan->argtypes,
 							   plan->nargs,
+							   NULL,
+							   NULL,
 							   plan->parserSetup,
 							   plan->parserSetupArg,
 							   plan->cursor_options,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 4cb1744da6..8fbe0f5f8f 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4288,6 +4288,8 @@ raw_expression_tree_walker(Node *node,
 					return true;
 				if (walker(coldef->compression, context))
 					return true;
+				if (walker(coldef->encryption, context))
+					return true;
 				if (walker(coldef->raw_default, context))
 					return true;
 				if (walker(coldef->collClause, context))
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 8ed2c4b8c7..6e1b24bd4b 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 0523013f53..681e36acfa 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -596,6 +596,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	TableConstraint TableLikeClause
 %type <ival>	TableLikeOptionList TableLikeOption
 %type <str>		column_compression opt_column_compression
+%type <list>	opt_column_encryption
 %type <list>	ColQualList
 %type <node>	ColConstraint ColConstraintElem ConstraintAttr
 %type <ival>	key_match
@@ -789,7 +790,7 @@ 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 EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
+	EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTION ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
 	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
@@ -814,7 +815,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 NESTED NEW NEXT NFC NFD NFKC NFKD NO
@@ -3778,13 +3779,14 @@ TypedTableElement:
 			| TableConstraint					{ $$ = $1; }
 		;
 
-columnDef:	ColId Typename opt_column_compression create_generic_options ColQualList
+columnDef:	ColId Typename opt_column_compression opt_column_encryption create_generic_options ColQualList
 				{
 					ColumnDef *n = makeNode(ColumnDef);
 
 					n->colname = $1;
 					n->typeName = $2;
 					n->compression = $3;
+					n->encryption = $4;
 					n->inhcount = 0;
 					n->is_local = true;
 					n->is_not_null = false;
@@ -3793,8 +3795,8 @@ columnDef:	ColId Typename opt_column_compression create_generic_options ColQualL
 					n->raw_default = NULL;
 					n->cooked_default = NULL;
 					n->collOid = InvalidOid;
-					n->fdwoptions = $4;
-					SplitColQualList($5, &n->constraints, &n->collClause,
+					n->fdwoptions = $5;
+					SplitColQualList($6, &n->constraints, &n->collClause,
 									 yyscanner);
 					n->location = @1;
 					$$ = (Node *) n;
@@ -3851,6 +3853,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 ColQualList:
 			ColQualList ColConstraint				{ $$ = lappend($1, $2); }
 			| /*EMPTY*/								{ $$ = NIL; }
@@ -6335,6 +6342,24 @@ DefineStmt:
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+			| CREATE COLUMN ENCRYPTION KEY any_name WITH VALUES definition
+				{
+					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; }
@@ -17749,6 +17774,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ERROR_P
 			| ESCAPE
@@ -17816,6 +17842,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -18320,6 +18347,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ERROR_P
@@ -18423,6 +18451,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 f668abfcb3..b45bf4c438 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -49,6 +49,8 @@ typedef struct VarParamState
 {
 	Oid		  **paramTypes;		/* array of parameter type OIDs */
 	int		   *numParams;		/* number of array entries */
+	Oid		  **paramOrigTbls;	/* underlying tables (0 if none) */
+	AttrNumber **paramOrigCols; /* underlying columns (0 if none) */
 } VarParamState;
 
 static Node *fixed_paramref_hook(ParseState *pstate, ParamRef *pref);
@@ -56,6 +58,7 @@ static Node *variable_paramref_hook(ParseState *pstate, ParamRef *pref);
 static Node *variable_coerce_param_hook(ParseState *pstate, Param *param,
 										Oid targetTypeId, int32 targetTypeMod,
 										int location);
+static void variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 static bool check_parameter_resolution_walker(Node *node, ParseState *pstate);
 static bool query_contains_extern_params_walker(Node *node, void *context);
 
@@ -81,15 +84,19 @@ setup_parse_fixed_parameters(ParseState *pstate,
  */
 void
 setup_parse_variable_parameters(ParseState *pstate,
-								Oid **paramTypes, int *numParams)
+								Oid **paramTypes, int *numParams,
+								Oid **paramOrigTbls, AttrNumber **paramOrigCols)
 {
 	VarParamState *parstate = palloc(sizeof(VarParamState));
 
 	parstate->paramTypes = paramTypes;
 	parstate->numParams = numParams;
+	parstate->paramOrigTbls = paramOrigTbls;
+	parstate->paramOrigCols = paramOrigCols;
 	pstate->p_ref_hook_state = (void *) parstate;
 	pstate->p_paramref_hook = variable_paramref_hook;
 	pstate->p_coerce_param_hook = variable_coerce_param_hook;
+	pstate->p_param_assign_orig_hook = variable_param_assign_orig_hook;
 }
 
 /*
@@ -145,14 +152,36 @@ variable_paramref_hook(ParseState *pstate, ParamRef *pref)
 	{
 		/* Need to enlarge param array */
 		if (*parstate->paramTypes)
+		{
 			*parstate->paramTypes = (Oid *) repalloc(*parstate->paramTypes,
 													 paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc(*parstate->paramOrigTbls,
+													paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc(*parstate->paramOrigCols,
+													paramno * sizeof(AttrNumber));
+		}
 		else
+		{
 			*parstate->paramTypes = (Oid *) palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc(paramno * sizeof(AttrNumber));
+		}
 		/* Zero out the previously-unreferenced slots */
 		MemSet(*parstate->paramTypes + *parstate->numParams,
 			   0,
 			   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigTbls)
+			MemSet(*parstate->paramOrigTbls + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigCols)
+			MemSet(*parstate->paramOrigCols + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(AttrNumber));
 		*parstate->numParams = paramno;
 	}
 
@@ -260,6 +289,18 @@ variable_coerce_param_hook(ParseState *pstate, Param *param,
 	return NULL;
 }
 
+static void
+variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol)
+{
+	VarParamState *parstate = (VarParamState *) pstate->p_ref_hook_state;
+	int			paramno = param->paramid;
+
+	if (parstate->paramOrigTbls)
+		(*parstate->paramOrigTbls)[paramno - 1] = origtbl;
+	if (parstate->paramOrigCols)
+		(*parstate->paramOrigCols)[paramno - 1] = origcol;
+}
+
 /*
  * Check for consistent assignment of variable parameters after completion
  * of parsing with parse_variable_parameters.
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 2a1d44b813..b964088044 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -595,6 +595,12 @@ transformAssignedExpr(ParseState *pstate,
 					 parser_errposition(pstate, exprLocation(orig_expr))));
 	}
 
+	if (IsA(expr, Param))
+	{
+		if (pstate->p_param_assign_orig_hook)
+			pstate->p_param_assign_orig_hook(pstate, castNode(Param, expr), RelationGetRelid(pstate->p_target_relation), attrno);
+	}
+
 	pstate->p_expr_kind = sv_expr_kind;
 
 	return expr;
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 6f18b68856..49078a733a 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -79,6 +79,7 @@
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
 
@@ -673,6 +674,8 @@ pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 								 const char *query_string,
 								 Oid **paramTypes,
 								 int *numParams,
+								 Oid **paramOrigTbls,
+								 AttrNumber **paramOrigCols,
 								 QueryEnvironment *queryEnv)
 {
 	Query	   *query;
@@ -687,7 +690,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.
@@ -1364,6 +1367,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	bool		is_named;
 	bool		save_log_statement_stats = log_statement_stats;
 	char		msec_str[32];
+	Oid		   *paramOrigTbls = palloc(numParams * sizeof(Oid));
+	AttrNumber *paramOrigCols = palloc(numParams * sizeof(AttrNumber));
 
 	/*
 	 * Report query to various monitoring facilities.
@@ -1484,6 +1489,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 */
@@ -1514,6 +1521,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 */
@@ -1811,6 +1820,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter corresponds to an encrypted column, but the parameter value was not encrypted")));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2607,8 +2626,41 @@ exec_describe_statement_message(const char *stmt_name)
 	for (int i = 0; i < psrc->num_params; i++)
 	{
 		Oid			ptype = psrc->param_types[i];
+		Oid			pcekid = InvalidOid;
+		int			pcekalg = 0;
+		int16		pflags = 0;
+
+		if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+		{
+			Oid			porigtbl = psrc->param_origtbls[i];
+			AttrNumber	porigcol = psrc->param_origcols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol <= 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("parameter %d corresponds to an encrypted column, but an underlying table and column could not be determined", i)));
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(porigtbl), Int16GetDatum(porigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", porigcol, porigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			ptype = orig_att->attrealtypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->attencalg;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x01;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		pq_sendint32(&row_description_buf, (int) pcekid);
+		pq_sendint16(&row_description_buf, pcekalg);
+		pq_sendint16(&row_description_buf, pflags);
 	}
 	pq_endmessage_reuse(&row_description_buf);
 
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 6b0a865262..0880796cf9 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"
@@ -1441,6 +1442,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,
@@ -2760,6 +2769,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;
diff --git a/src/backend/utils/adt/arrayfuncs.c b/src/backend/utils/adt/arrayfuncs.c
index b0c37ede87..5683731188 100644
--- a/src/backend/utils/adt/arrayfuncs.c
+++ b/src/backend/utils/adt/arrayfuncs.c
@@ -3387,6 +3387,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/plancache.c b/src/backend/utils/cache/plancache.c
index 0d6a295674..f6893dd8b7 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -340,6 +340,8 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 				   MemoryContext querytree_context,
 				   Oid *param_types,
 				   int num_params,
+				   Oid *param_origtbls,
+				   AttrNumber *param_origcols,
 				   ParserSetupHook parserSetup,
 				   void *parserSetupArg,
 				   int cursor_options,
@@ -419,6 +421,14 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 	{
 		plansource->param_types = (Oid *) palloc(num_params * sizeof(Oid));
 		memcpy(plansource->param_types, param_types, num_params * sizeof(Oid));
+
+		plansource->param_origtbls = (Oid *) palloc0(num_params * sizeof(Oid));
+		if (param_origtbls)
+			memcpy(plansource->param_origtbls, param_origtbls, num_params * sizeof(Oid));
+
+		plansource->param_origcols = (AttrNumber *) palloc0(num_params * sizeof(AttrNumber));
+		if (param_origcols)
+			memcpy(plansource->param_origcols, param_origcols, num_params * sizeof(AttrNumber));
 	}
 	else
 		plansource->param_types = NULL;
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 1912b12146..9a27198087 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,33 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		256
 	},
+	{
+		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 +319,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/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e4fdb6b75b..a4ab76c847 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8073,6 +8073,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcekname;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8182,17 +8183,24 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	if (fout->remoteVersion >= 120000)
 		appendPQExpBufferStr(q,
-							 "a.attgenerated\n");
+							 "a.attgenerated,\n");
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "'' AS attgenerated,\n");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBufferStr(q,
+							 "(SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek ) AS attcekname\n");
+	else
+		appendPQExpBufferStr(q,
+							 "NULL AS attcekname\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
 					  "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
 					  "JOIN pg_catalog.pg_attribute a ON (src.tbloid = a.attrelid) "
 					  "LEFT JOIN pg_catalog.pg_type t "
-					  "ON (a.atttypid = t.oid)\n"
+					  "ON (CASE WHEN a.attrealtypid <> 0 THEN a.attrealtypid ELSE a.atttypid END = t.oid)\n"
 					  "WHERE a.attnum > 0::pg_catalog.int2\n"
 					  "ORDER BY a.attrelid, a.attnum",
 					  tbloids->data);
@@ -8211,6 +8219,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcekname = PQfnumber(res, "attcekname");
 	i_attisdropped = PQfnumber(res, "attisdropped");
 	i_attlen = PQfnumber(res, "attlen");
 	i_attalign = PQfnumber(res, "attalign");
@@ -8272,6 +8281,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		tbinfo->typstorage = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attidentity = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attgenerated = (char *) pg_malloc(numatts * sizeof(char));
+		tbinfo->attcekname = (char **) pg_malloc(numatts * sizeof(char *));
 		tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char));
@@ -8300,6 +8310,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->attidentity[j] = *(PQgetvalue(res, r, i_attidentity));
 			tbinfo->attgenerated[j] = *(PQgetvalue(res, r, i_attgenerated));
 			tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
+			if (!PQgetisnull(res, r, i_attcekname))
+				tbinfo->attcekname[j] = pg_strdup(PQgetvalue(res, r, i_attcekname));
+			else
+				tbinfo->attcekname[j] = NULL;
 			tbinfo->attisdropped[j] = (PQgetvalue(res, r, i_attisdropped)[0] == 't');
 			tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
 			tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
@@ -15267,6 +15281,12 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcekname[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s)",
+										  fmtId(tbinfo->attcekname[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..2c176ed13c 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -333,6 +333,7 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	char	  **attcekname;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
 	char	   *attalign;		/* attribute align, used by binary_upgrade */
 	bool	   *attislocal;		/* true if attr has local definition */
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 0955142215..663788ef64 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 9f95869eca..8fe055ffed 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1284,6 +1284,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;
 
@@ -1448,7 +1456,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 88d92a08ae..fe81547b29 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1498,7 +1498,7 @@ describeOneTableDetails(const char *schemaname,
 	bool		printTableInitialized = false;
 	int			i;
 	char	   *view_def = NULL;
-	char	   *headers[12];
+	char	   *headers[13];
 	PQExpBufferData title;
 	PQExpBufferData tmpbuf;
 	int			cols;
@@ -1514,6 +1514,7 @@ describeOneTableDetails(const char *schemaname,
 				fdwopts_col = -1,
 				attstorage_col = -1,
 				attcompression_col = -1,
+				attcekname_col = -1,
 				attstattarget_col = -1,
 				attdescr_col = -1;
 	int			numrows;
@@ -1812,7 +1813,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)
@@ -1826,7 +1827,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");
@@ -1877,6 +1878,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 ||
@@ -2000,6 +2010,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)
@@ -2092,6 +2104,17 @@ describeOneTableDetails(const char *schemaname,
 							  false, false);
 		}
 
+		/* Column encryption */
+		if (attcekname_col >= 0)
+		{
+			if (!PQgetisnull(res, i, attcekname_col))
+				printTableAddCell(&cont, PQgetvalue(res, i, attcekname_col),
+								  false, false);
+			else
+				printTableAddCell(&cont, "",
+								  false, false);
+		}
+
 		/* Statistics target, if the relkind supports this feature */
 		if (attstattarget_col >= 0)
 			printTableAddCell(&cont, PQgetvalue(res, i, attstattarget_col),
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index f8ce1a0706..9f4fce26fd 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -195,6 +195,7 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\g [(OPTIONS)] [FILE]  execute query (and send result to file or |pipe);\n"
 		  "                         \\g with no arguments is equivalent to a semicolon\n");
 	HELP0("  \\gdesc                 describe result of query, without executing it\n");
+	HELP0("  \\gencr [PARAM]...      execute query, with support for parameter encryption\n");
 	HELP0("  \\gexec                 execute query, then execute each value in its result\n");
 	HELP0("  \\gset [PREFIX]         execute query and store result in psql variables\n");
 	HELP0("  \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n");
@@ -412,6 +413,8 @@ helpVariables(unsigned short int pager)
 		  "    true if last query failed, else false\n");
 	HELP0("  FETCH_COUNT\n"
 		  "    the number of result rows to fetch and display at a time (0 = unlimited)\n");
+	HELP0("  HIDE_COLUMN_ENCRYPTION\n"
+		  "    if set, column encryption details are not displayed\n");
 	HELP0("  HIDE_TABLEAM\n"
 		  "    if set, table access methods are not displayed\n");
 	HELP0("  HIDE_TOAST_COMPRESSION\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 2399cffa3f..636729df1d 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -95,6 +95,10 @@ typedef struct _psqlSettings
 
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
 	bool		gdesc_flag;		/* one-shot request to describe query result */
+	bool		gencr_flag;		/* one-shot request to send query with support
+								 * for parameter encryption */
+	int			num_params;		/* number of query parameters */
+	char	  **params;			/* query parameters */
 	bool		gexec_flag;		/* one-shot request to execute query result */
 	bool		crosstab_flag;	/* one-shot request to crosstab result */
 	char	   *ctv_args[4];	/* \crosstabview arguments */
@@ -134,6 +138,7 @@ typedef struct _psqlSettings
 	bool		quiet;
 	bool		singleline;
 	bool		singlestep;
+	bool		hide_column_encryption;
 	bool		hide_compression;
 	bool		hide_tableam;
 	int			fetch_count;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 7c2f555f15..c955222a50 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -1188,6 +1188,13 @@ hide_compression_hook(const char *newval)
 							 &pset.hide_compression);
 }
 
+static bool
+hide_column_encryption_hook(const char *newval)
+{
+	return ParseVariableBool(newval, "HIDE_COLUMN_ENCRYPTION",
+							 &pset.hide_column_encryption);
+}
+
 static bool
 hide_tableam_hook(const char *newval)
 {
@@ -1259,6 +1266,9 @@ EstablishVariableSpace(void)
 	SetVariableHooks(pset.vars, "SHOW_CONTEXT",
 					 show_context_substitute_hook,
 					 show_context_hook);
+	SetVariableHooks(pset.vars, "HIDE_COLUMN_ENCRYPTION",
+					 bool_substitute_hook,
+					 hide_column_encryption_hook);
 	SetVariableHooks(pset.vars, "HIDE_TOAST_COMPRESSION",
 					 bool_substitute_hook,
 					 hide_compression_hook);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index e572f585ef..02e8a3512b 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1692,7 +1692,7 @@ psql_completion(const char *text, int start, int end)
 		"\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding",
 		"\\endif", "\\errverbose", "\\ev",
 		"\\f",
-		"\\g", "\\gdesc", "\\getenv", "\\gexec", "\\gset", "\\gx",
+		"\\g", "\\gdesc", "\\gencr", "\\getenv", "\\gexec", "\\gset", "\\gx",
 		"\\help", "\\html",
 		"\\if", "\\include", "\\include_relative", "\\ir",
 		"\\list", "\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index b5fd72a4ac..5a065eb80b 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..fabad38ac0 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,7 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPrepared2           187
+PQsendQueryPrepared2      188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index dc49387d6c..f3c15ece4d 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -344,6 +344,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	{"cmklookup", "PGCMKLOOKUP", "", NULL,
+		"CMK-Lookup", "", 64,
+	offsetof(struct pg_conn, cmklookup)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -4097,6 +4101,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 0000000000..524ba8a0f9
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,758 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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;
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, mac_key, mac_key_len);
+	if (!pkey)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("could not allocate key for HMAC: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out out memory");
+		goto fail;
+	}
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(cekalg);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	*(int64 *) (buf + PG_AD_LEN + encrlen) = pg_hton64(PG_AD_LEN * 8);
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, buf, bufsize))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, md_len_p))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	Assert(*md_len_p == md_hash_length(md));
+
+	/* truncate output to half the length, per spec */
+	*md_len_p /= 2;
+
+	result = true;
+fail:
+	free(buf);
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+
+unsigned char *
+decrypt_value(PGresult *res, const PGCEK *cek, int cekalg, const unsigned char *input, int inputlen, const char **errmsgp)
+{
+	static char msgbuf[1024];
+
+	const unsigned char *iv = NULL;
+	size_t		ivlen;
+
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *decr;
+	int			decrlen,
+				decrlen2;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized encryption algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized digest algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("column encryption key has wrong length for algorithm (has: %zu, required: %d)"),
+				 cek->cekdatalen, key_len);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key = cek->cekdata + mac_key_len;
+	mac_key = cek->cekdata;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  cekalg,
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	if (memcmp(input + (inputlen - md_len), md_value, md_len) != 0)
+	{
+		*errmsgp = libpq_gettext("MAC mismatch");
+		goto fail;
+	}
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	iv = input;
+	input += ivlen;
+	inputlen -= ivlen;
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = inputlen + EVP_CIPHER_CTX_block_size(evp_cipher_ctx) + 1;
+#ifndef TEST_ENCRYPT
+	buf = pqResultAlloc(res, bufsize, false);
+#else
+	buf = malloc(bufsize);
+#endif
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out out memory");
+		goto fail;
+	}
+	decr = buf;
+	if (!EVP_DecryptUpdate(evp_cipher_ctx, decr, &decrlen, input, inputlen - md_len))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	if (!EVP_DecryptFinal_ex(evp_cipher_ctx, decr + decrlen, &decrlen2))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	decrlen += decrlen2;
+	Assert(decrlen < bufsize);
+	decr[decrlen] = '\0';
+	result = decr;
+
+fail:
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	return result;
+}
+
+#ifndef TEST_ENCRYPT
+static bool
+make_siv(PGconn *conn,
+		 unsigned char *iv, size_t ivlen,
+		 const EVP_MD *md,
+		 const unsigned char *iv_key, int iv_key_len,
+		 const unsigned char *plaintext, int plaintext_len)
+{
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	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 0000000000..b0093dc527
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * encryption support
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FE_ENCRYPT_H
+#define FE_ENCRYPT_H
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+extern unsigned char *decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg,
+											int fromlen, const unsigned char *from,
+											int *tolen);
+
+extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg,
+									const unsigned char *input, int inputlen,
+									const char **errmsgp);
+
+extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg,
+									const unsigned char *value, int *nbytesp, bool enc_det);
+
+#endif							/* FE_ENCRYPT_H */
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index e20d6177fe..bd8052d61b 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,381 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+int
+pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+					  const char *keyrealm)
+{
+	char	   *keyname_copy;
+	char	   *keyrealm_copy;
+	bool		found;
+
+	keyname_copy = strdup(keyname);
+	if (!keyname_copy)
+		return EOF;
+	keyrealm_copy = strdup(keyrealm);
+	if (!keyrealm_copy)
+	{
+		free(keyname_copy);
+		return EOF;
+	}
+
+	found = false;
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		struct pg_cmk *checkcmk = &conn->cmks[i];
+
+		/* replace existing? */
+		if (checkcmk->cmkid == keyid)
+		{
+			free(checkcmk->cmkname);
+			free(checkcmk->cmkrealm);
+			checkcmk->cmkname = keyname_copy;
+			checkcmk->cmkrealm = keyrealm_copy;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newncmks;
+		struct pg_cmk *newcmks;
+		struct pg_cmk *newcmk;
+
+		newncmks = conn->ncmks + 1;
+		if (newncmks <= 0)
+			return EOF;
+		newcmks = realloc(conn->cmks, newncmks * sizeof(struct pg_cmk));
+		if (!newcmks)
+		{
+			free(keyname_copy);
+			free(keyrealm_copy);
+			return EOF;
+		}
+
+		newcmk = &newcmks[newncmks - 1];
+		newcmk->cmkid = keyid;
+		newcmk->cmkname = keyname_copy;
+		newcmk->cmkrealm = keyrealm_copy;
+
+		conn->ncmks = newncmks;
+		conn->cmks = newcmks;
+	}
+
+	return 0;
+}
+
+/*
+ * Replace placeholders in input string.  Return value malloc'ed.
+ */
+static char *
+replace_cmk_placeholders(const char *in, const char *cmkname, const char *cmkrealm, int cmkalg, const char *b64data)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s;
+
+						switch (cmkalg)
+						{
+							case PG_CMK_RSAES_OAEP_SHA_1:
+								s = "RSAES_OAEP_SHA_1";
+								break;
+							case PG_CMK_RSAES_OAEP_SHA_256:
+								s = "RSAES_OAEP_SHA_256";
+								break;
+							default:
+								s = "INVALID";
+						}
+						appendPQExpBufferStr(&buf, s);
+					}
+					p++;
+					break;
+				case 'b':
+					appendPQExpBufferStr(&buf, b64data);
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'r':
+					appendPQExpBufferStr(&buf, cmkrealm);
+					p++;
+					break;
+				default:
+					appendPQExpBufferChar(&buf, p[0]);
+			}
+		}
+		else
+			appendPQExpBufferChar(&buf, p[0]);
+	}
+
+	return buf.data;
+}
+
+#ifndef USE_SSL
+/*
+ * Dummy implementation for non-SSL builds
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	appendPQExpBuffer(&conn->errorMessage,
+					  libpq_gettext("column encryption not supported by this build\n"));
+	return NULL;
+}
+#endif
+
+/*
+ * Decrypt a CEK using the given CMK.  The ciphertext is passed in
+ * "from" and "fromlen".  Return the decrypted value in a malloc'ed area, its
+ * length via "tolen".  Return NULL on error; add error messages directly to
+ * "conn".
+ */
+static unsigned char *
+decrypt_cek(PGconn *conn, const PGCMK *cmk, int cmkalg,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	char	   *cmklookup;
+	bool		found = false;
+	unsigned char *result = NULL;
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+		return NULL;
+	}
+
+	/*
+	 * Analyze semicolon-separated list
+	 */
+	for (char *s = strtok(cmklookup, ";"); s; s = strtok(NULL, ";"))
+	{
+		char	   *sep;
+
+		/* split found token at '=' */
+		sep = strchr(s, '=');
+		if (!sep)
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("syntax error in CMK lookup specification, missing \"%c\": %s\n"), '=', s);
+			break;
+		}
+
+		/* matching realm? */
+		if (strncmp(s, "*", sep - s) == 0 || strncmp(s, cmk->cmkrealm, sep - s) == 0)
+		{
+			char	   *sep2;
+
+			found = true;
+
+			sep2 = strchr(sep, ':');
+			if (!sep2)
+			{
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("syntax error in CMK lookup specification, missing \"%c\": %s\n"), ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+				FILE	   *fp;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				fp = fopen(cmkfilename, "rb");
+				if (fp)
+				{
+					result = decrypt_cek_from_file(conn, fp, cmkalg, fromlen, from, tolen);
+					fclose(fp);
+				}
+				else
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not open file \"%s\": %m\n"), cmkfilename);
+					free(cmkfilename);
+					goto fail;
+				}
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				int			enclen;
+				char	   *enc;
+				char	   *command;
+				FILE	   *fp;
+				char		buf[4096];
+
+				enclen = pg_b64_enc_len(fromlen);
+				enc = malloc(enclen + 1);
+				if (!enc)
+				{
+					appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+					goto fail;
+				}
+				enclen = pg_b64_encode((const char *) from, fromlen, enc, enclen);
+				if (enclen < 0)
+				{
+					appendPQExpBuffer(&conn->errorMessage, libpq_gettext("base64 encoding failed\n"));
+					free(enc);
+					goto fail;
+				}
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, enc);
+				free(enc);
+				fp = popen(command, "r");
+				free(command);
+				if (!fp)
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not run command \"%s\": %m\n"), command);
+					goto fail;
+				}
+				if (fgets(buf, sizeof(buf), fp))
+				{
+					int			linelen;
+					int			declen;
+					char	   *dec;
+
+					linelen = strlen(buf);
+					if (buf[linelen - 1] == '\n')
+					{
+						buf[linelen - 1] = '\0';
+						linelen--;
+					}
+					declen = pg_b64_dec_len(linelen);
+					dec = malloc(declen);
+					if (!dec)
+					{
+						appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+						goto fail;
+					}
+					declen = pg_b64_decode(buf, linelen, dec, declen);
+					if (declen < 0)
+					{
+						appendPQExpBuffer(&conn->errorMessage,
+										  libpq_gettext("base64 decoding failed\n"));
+						free(dec);
+						goto fail;
+					}
+					result = (unsigned char *) dec;
+					*tolen = declen;
+				}
+				else
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not read from command: %m\n"));
+					pclose(fp);
+					goto fail;
+				}
+				pclose(fp);
+			}
+			else
+			{
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("CMK lookup scheme \"%s\" not recognized\n"), sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("no CMK lookup found for realm \"%s\"\n"), cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+int
+pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, const unsigned char *value, int len)
+{
+	PGCMK	   *cmk = NULL;
+	unsigned char *plainval = NULL;
+	int			plainvallen = 0;
+	bool		found;
+
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		if (conn->cmks[i].cmkid == cmkid)
+		{
+			cmk = &conn->cmks[i];
+			break;
+		}
+	}
+
+	if (!cmk)
+		return EOF;
+
+	plainval = decrypt_cek(conn, cmk, cmkalg, len, value, &plainvallen);
+	if (!plainval)
+		return EOF;
+
+	found = false;
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		struct pg_cek *checkcek = &conn->ceks[i];
+
+		/* replace existing? */
+		if (checkcek->cekid == keyid)
+		{
+			free(checkcek->cekdata);
+			checkcek->cekdata = plainval;
+			checkcek->cekdatalen = plainvallen;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newnceks;
+		struct pg_cek *newceks;
+		struct pg_cek *newcek;
+
+		newnceks = conn->nceks + 1;
+		if (newnceks <= 0)
+		{
+			free(plainval);
+			return EOF;
+		}
+		newceks = realloc(conn->ceks, newnceks * sizeof(struct pg_cek));
+		if (!newceks)
+		{
+			free(plainval);
+			return EOF;
+		}
+
+		newcek = &newceks[newnceks - 1];
+		newcek->cekid = keyid;
+		newcek->cekdata = plainval;
+		newcek->cekdatalen = plainvallen;
+
+		conn->nceks = newnceks;
+		conn->ceks = newceks;
+	}
+
+	return 0;
+}
 
 /*
  * pqRowProcessor
@@ -1250,9 +1630,58 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 		}
 		else
 		{
-			bool		isbinary = (res->attDescs[i].format != 0);
+			bool		isbinary = ((res->attDescs[i].format & 0x0F) != 0);
 			char	   *val;
 
+			if (res->attDescs[i].cekid)
+			{
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == res->attDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					*errmsgp = libpq_gettext("column encryption key not found");
+					goto fail;
+				}
+
+				if (isbinary)
+				{
+					val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+												 (const unsigned char *) columns[i].value, clen, errmsgp);
+				}
+				else
+				{
+					unsigned char *buf;
+					unsigned char *enccolval;
+					size_t		enccolvallen;
+
+					buf = malloc(clen + 1);
+					memcpy(buf, columns[i].value, clen);
+					buf[clen] = '\0';
+					enccolval = PQunescapeBytea(buf, &enccolvallen);
+					val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+												 enccolval, enccolvallen, errmsgp);
+					free(enccolval);
+					free(buf);
+				}
+
+				if (val == NULL)
+					goto fail;
+#else
+				*errmsgp = libpq_gettext("column encryption not supported by this build");
+				goto fail;
+#endif
+			}
+			else
+			{
 			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
 			if (val == NULL)
 				goto fail;
@@ -1260,6 +1689,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;
@@ -1586,7 +2016,9 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   NULL,
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1704,6 +2136,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;
@@ -1731,7 +2177,9 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   paramForceColumnEncryptions,
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1832,7 +2280,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;
@@ -1879,14 +2329,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;
 		}
 	}
@@ -1905,6 +2394,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1923,9 +2413,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
 		{
@@ -2379,12 +2914,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);
 }
@@ -3682,6 +4232,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)
 {
@@ -3867,6 +4428,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 f267dfd33c..19ec572764 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -45,6 +45,8 @@ static int	getRowDescriptions(PGconn *conn, int msgLength);
 static int	getParamDescriptions(PGconn *conn, int msgLength);
 static int	getAnotherTuple(PGconn *conn, int msgLength);
 static int	getParameterStatus(PGconn *conn);
+static int	getColumnMasterKey(PGconn *conn);
+static int	getColumnEncryptionKey(PGconn *conn);
 static int	getNotify(PGconn *conn);
 static int	getCopyStart(PGconn *conn, ExecStatusType copytype);
 static int	getReadyForQuery(PGconn *conn);
@@ -319,6 +321,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 &&
@@ -576,6 +594,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) ||
@@ -583,7 +603,9 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			pqGetInt(&typid, 4, conn) ||
 			pqGetInt(&typlen, 2, conn) ||
 			pqGetInt(&atttypmod, 4, conn) ||
-			pqGetInt(&format, 2, conn))
+			pqGetInt(&format, 2, conn) ||
+			pqGetInt(&cekid, 4, conn) ||
+			pqGetInt(&cekalg, 2, conn))
 		{
 			/* We should not run out of data here, so complain */
 			errmsg = libpq_gettext("insufficient data in \"T\" message");
@@ -611,8 +633,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;
 	}
 
@@ -714,10 +738,22 @@ getParamDescriptions(PGconn *conn, int msgLength)
 	for (i = 0; i < nparams; i++)
 	{
 		int			typid;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGetInt(&typid, 4, conn))
 			goto not_enough_data;
 		result->paramDescs[i].typid = typid;
+		if (pqGetInt(&cekid, 4, conn))
+			goto not_enough_data;
+		result->paramDescs[i].cekid = cekid;
+		if (pqGetInt(&cekalg, 2, conn))
+			goto not_enough_data;
+		result->paramDescs[i].cekalg = cekalg;
+		if (pqGetInt(&flags, 2, conn))
+			goto not_enough_data;
+		result->paramDescs[i].flags = flags;
 	}
 
 	/* Success! */
@@ -1443,6 +1479,89 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+/*
+ * Attempt to read a ColumnMasterKey message.
+ * Entry: 'y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	int			keyid;
+	char	   *keyname;
+	char	   *keyrealm;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	if (!keyname)
+		return EOF;
+	/* Get the key realm */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyrealm = strdup(conn->workBuffer.data);
+	if (!keyrealm)
+		return EOF;
+	/* And save it */
+	ret = pqSaveColumnMasterKey(conn, keyid, keyname, keyrealm);
+
+	free(keyname);
+	free(keyrealm);
+
+	return ret;
+}
+
+/*
+ * Attempt to read a ColumnEncryptionKey message.
+ * Entry: 'Y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	int			keyid;
+	int			cmkid;
+	int			cmkalg;
+	char	   *buf;
+	int			vallen;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK ID */
+	if (pqGetInt(&cmkid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK algorithm */
+	if (pqGetInt(&cmkalg, 2, conn) != 0)
+		return EOF;
+	/* Get the key data len */
+	if (pqGetInt(&vallen, 4, conn) != 0)
+		return EOF;
+	/* Get the key data */
+	buf = malloc(vallen);
+	if (!buf)
+		return EOF;
+	if (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	if (pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	free(buf);
+	return 0;
+}
 
 /*
  * Attempt to read a Notify response message.
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index 5d68cf2eb3..e08cbcff33 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -458,7 +458,12 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 	nfields = pqTraceOutputInt16(f, message, cursor);
 
 	for (int i = 0; i < nfields; i++)
+	{
+		pqTraceOutputInt32(f, message, cursor, regress);
 		pqTraceOutputInt32(f, message, cursor, regress);
+		pqTraceOutputInt16(f, message, cursor);
+		pqTraceOutputInt16(f, message, cursor);
+	}
 }
 
 /* RowDescription */
@@ -479,6 +484,8 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
 		pqTraceOutputInt16(f, message, cursor);
 		pqTraceOutputInt32(f, message, cursor, false);
 		pqTraceOutputInt16(f, message, cursor);
+		pqTraceOutputInt32(f, message, cursor, regress);
+		pqTraceOutputInt16(f, message, cursor);
 	}
 }
 
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 7986445f1a..42fbe0cd49 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -267,6 +267,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -438,6 +440,15 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPrepared2(PGconn *conn,
+								 const char *stmtName,
+								 int nParams,
+								 const char *const *paramValues,
+								 const int *paramLengths,
+								 const int *paramFormats,
+								 const int *paramForceColumnEncryptions,
+								 int resultFormat,
+								 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +472,15 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPrepared2(PGconn *conn,
+								 const char *stmtName,
+								 int nParams,
+								 const char *const *paramValues,
+								 const int *paramLengths,
+								 const int *paramFormats,
+								 const int *paramForceColumnEncryptions,
+								 int resultFormat,
+								 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +551,7 @@ extern int	PQfformat(const PGresult *res, int field_num);
 extern Oid	PQftype(const PGresult *res, int field_num);
 extern int	PQfsize(const PGresult *res, int field_num);
 extern int	PQfmod(const PGresult *res, int field_num);
+extern int	PQfisencrypted(const PGresult *res, int field_num);
 extern char *PQcmdStatus(PGresult *res);
 extern char *PQoidStatus(const PGresult *res);	/* old and ugly */
 extern Oid	PQoidValue(const PGresult *res);	/* new and improved */
@@ -540,6 +561,7 @@ extern int	PQgetlength(const PGresult *res, int tup_num, int field_num);
 extern int	PQgetisnull(const PGresult *res, int tup_num, int field_num);
 extern int	PQnparams(const PGresult *res);
 extern Oid	PQparamtype(const PGresult *res, int param_num);
+extern int	PQparamisencrypted(const PGresult *res, int param_num);
 
 /* Describe prepared statements and portals */
 extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 51ab51f9f9..ed803b1f88 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -111,6 +111,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -342,6 +345,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.
@@ -395,6 +418,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *cmklookup;		/* CMK lookup specification */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -477,6 +501,12 @@ struct pg_conn
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
 
+	/* Column encryption support data */
+	int			ncmks;
+	PGCMK	   *cmks;
+	int			nceks;
+	PGCEK	   *ceks;
+
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
 	int			inBufSize;		/* allocated size of buffer */
@@ -672,6 +702,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern int	pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+								  const char *keyrealm);
+extern int	pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg,
+									  const unsigned char *value, int len);
 extern int	pqRowProcessor(PGconn *conn, const char **errmsgp);
 extern void pqCommandQueueAdvance(PGconn *conn);
 extern int	PQsendQueryContinue(PGconn *conn, const char *query);
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 1f62ba1b57..844e085e6d 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
 AVAIL_LANGUAGES  = cs de el es fr he it ja ko pl pt_BR ru sv tr uk zh_CN zh_TW
-GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
+GETTEXT_FILES    = fe-auth.c fe-auth-scram.c fe-connect.c fe-encrypt-openssl.c fe-exec.c fe-gssapi-common.c fe-lobj.c fe-misc.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-gssapi.c fe-secure-openssl.c win32.c ../../port/thread.c
 GETTEXT_TRIGGERS = libpq_gettext pqInternalNotice:2
 GETTEXT_FLAGS    = libpq_gettext:1:pass-c-format pqInternalNotice:2:c-format
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 5f2ef88cf3..a03047e8fa 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9532,7 +9532,7 @@ DO $d$
     END;
 $d$;
 ERROR:  invalid option "password"
-HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
+HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, cmklookup, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
 CONTEXT:  SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
 PL/pgSQL function inline_code_block line 3 at EXECUTE
 -- If we add a password for our user mapping instead, we should get a different

base-commit: d3117fc1a3e87717a57be0153408e5387e265e1b
-- 
2.37.0

#19Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#18)
Re: Transparent column encryption

On 7/12/22 11:29, Peter Eisentraut wrote:

Updated patch, to resolve some merge conflicts.

Thank you for working on this; it's an exciting feature.

The CEK key
material is in turn encrypted by an assymmetric key called the column
master key (CMK).

I'm not yet understanding why the CMK is asymmetric. Maybe you could use
the public key to add ephemeral, single-use encryption keys that no one
but the private key holder could use (after you forget them on your
side, that is). But since the entire column is encrypted with a single
CEK, you would essentially only be able to do that if you created an
entirely new column or table; do I have that right?

I'm used to public keys being safe for... publication, but if I'm
understanding correctly, it's important that the server admin doesn't
get hold of the public key for your CMK, because then they could
substitute their own CEKs transparently and undermine future encrypted
writes. That seems surprising. Am I just missing something important
about RSAES-OAEP?

+#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

It looks like these ciphersuites were abandoned by the IETF. Are there
existing implementations of them that have been audited/analyzed? Are
they safe (and do we know that the claims made in the draft are
correct)? How do they compare to other constructions like AES-GCM-SIV
and XChacha20-Poly1305?

+-- \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)

I'd expect \gencr to error out without sending plaintext. I know that
under the hood this is just setting up a prepared statement, but if I'm
using \gencr, presumably I really do want to be encrypting my data.
Would it be a problem to always set force-column-encryption for the
parameters we're given here? Any unencrypted columns could be provided
directly.

Another idle thought I had was that it'd be nice to have some syntax for
providing a null value to \gencr (assuming I didn't overlook it in the
patch). But that brings me to...

+  <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>

This is a major gap, IMO. Especially with the switch to authenticated
ciphers, because it means you can't sign your NULL values. And having
each client or user that's out there solve this with a magic in-band
value seems like a recipe for pain.

Since we're requiring "canonical" use of text format, and the docs say
there are no embedded or trailing nulls allowed in text values, could we
steal the use of a single zero byte to mean NULL? One additional
complication would be that the client would have to double-check that
we're not writing a NULL into a NOT NULL column, and complain if it
reads one during decryption. Another complication would be that the
client would need to complain if it got a plaintext NULL.

(The need for robust client-side validation of encrypted columns might
be something to expand on in the docs more generally, since before this
feature, it could probably be assumed that the server was buggy if it
sent you unparsable junk in a column.)

+   <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>

Is this AD intended as a placeholder for the future, or does it serve a
particular purpose?

Thanks,
--Jacob

#20Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#19)
Re: Transparent column encryption

On 15.07.22 19:47, Jacob Champion wrote:

The CEK key
material is in turn encrypted by an assymmetric key called the column
master key (CMK).

I'm not yet understanding why the CMK is asymmetric.

I'm not totally sure either. I started to build it that way because
other systems were doing it that way, too. But I have been thinking
about adding a symmetric alternative for the CMKs as well (probably AESKW).

I think there are a couple of reasons why asymmetric keys are possibly
useful for CMKs:

Some other products make use of secure enclaves to do computations on
(otherwise) encrypted values on the server. I don't fully know how that
works, but I suspect that asymmetric keys can play a role in that. (I
don't have any immediate plans for that in my patch. It seems to be a
dying technology at the moment.)

Asymmetric keys gives you some more options for how you set up the keys
at the beginning. For example, you create the asymmetric key pair on
the host where your client program that wants access to the encrypted
data will run. You put the private key in an appropriate location for
run time. You send the public key to another host. On that other host,
you create the CEK, encrypt it with the CMK, and then upload it into the
server (CREATE COLUMN ENCRYPTION KEY). Then you can wipe that second
host. That way, you can be even more sure that the unencrypted CEK
isn't left anywhere. I'm not sure whether this method is very useful in
practice, but it's interesting.

In any case, as I mentioned above, this particular aspect is up for
discussion.

Also note that if you use a KMS (cmklookup "run" method), the actual
algorithm doesn't even matter (depending on details of the KMS setup),
since you just tell the KMS "decrypt this", and the KMS knows by itself
what algorithm to use. Maybe there should be a way to specify "unknown"
in the ckdcmkalg field.

+#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

It looks like these ciphersuites were abandoned by the IETF. Are there
existing implementations of them that have been audited/analyzed? Are
they safe (and do we know that the claims made in the draft are
correct)? How do they compare to other constructions like AES-GCM-SIV
and XChacha20-Poly1305?

The short answer is, these same algorithms are used in equivalent
products (see MS SQL Server, MongoDB). They even reference the same
exact draft document.

Besides that, here is my analysis for why these are good choices: You
can't use any of the counter modes, because since the encryption happens
on the client, there is no way to coordinate to avoid nonce reuse. So
among mainstream modes, you are basically left with AES-CBC with a
random IV. In that case, even if you happen to reuse an IV, the
possible damage is very contained.

And then, if you want to use AEAD, you combine that with some MAC, and
HMAC is just as good as any for that.

The referenced draft document doesn't really contain any additional
cryptographic insights, it's just a guide on a particular way to put
these two together.

So altogether I think this is a pretty solid choice.

+-- \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)

I'd expect \gencr to error out without sending plaintext. I know that
under the hood this is just setting up a prepared statement, but if I'm
using \gencr, presumably I really do want to be encrypting my data.
Would it be a problem to always set force-column-encryption for the
parameters we're given here? Any unencrypted columns could be provided
directly.

Yeah, this needs a bit of refinement. You don't want something named
"encr" but it only encrypts some of the time. We could possibly do what
you suggest and make it set the force-encryption flag, or maybe rename
it or add another command that just uses prepared statements and doesn't
promise anything about encryption from its name.

This also ties in with how pg_dump will eventually work. I think by
default pg_dump will just dump things encrypted and set it up so that
COPY writes it back encrypted. But there should probably be a mode that
dumps out plaintext and then uses one of these commands to load the
plaintext back in. What these psql commands need to do also depends on
what pg_dump needs them to do.

+  <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>

This is a major gap, IMO. Especially with the switch to authenticated
ciphers, because it means you can't sign your NULL values. And having
each client or user that's out there solve this with a magic in-band
value seems like a recipe for pain.

Since we're requiring "canonical" use of text format, and the docs say
there are no embedded or trailing nulls allowed in text values, could we
steal the use of a single zero byte to mean NULL? One additional
complication would be that the client would have to double-check that
we're not writing a NULL into a NOT NULL column, and complain if it
reads one during decryption. Another complication would be that the
client would need to complain if it got a plaintext NULL.

You're already alluding to some of the complications. Also consider
that null values could arise from, say, outer joins. So you could be in
a situation where encrypted and unencrypted null values coexist. And of
course the server doesn't know about the encrypted null values. So how
do you maintain semantics, like for aggregate functions, primary keys,
anything that treats null values specially? How do clients deal with a
mix of encrypted and unencrypted null values, how do they know which one
is real. What if the client needs to send a null value back as a
parameter? All of this would create enormous complications, if they can
be solved at all.

I think a way to look at this is that this column encryption feature
isn't suitable for disguising the existence or absence of data, it can
only disguise the particular data that you know exists.

+   <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>

Is this AD intended as a placeholder for the future, or does it serve a
particular purpose?

It has been recommended that you include the identity of the encryption
algorithm in the AD. This protects the client from having to decrypt
stuff that wasn't meant to be decrypted (in that way).

#21Robert Haas
robertmhaas@gmail.com
In reply to: Peter Eisentraut (#20)
Re: Transparent column encryption

On Mon, Jul 18, 2022 at 6:53 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

I think a way to look at this is that this column encryption feature
isn't suitable for disguising the existence or absence of data, it can
only disguise the particular data that you know exists.

+1.

Even there, what can be accomplished with a feature that only encrypts
individual column values is by nature somewhat limited. If you have a
text column that, for one row, stores the value 'a', and for some
other row, stores the entire text of Don Quixote in the original
Spanish, it is going to be really difficult to keep an adversary who
can read from the disk from distinguishing those rows. If you want to
fix that, you're going to need to do block-level encryption or
something of that sort. And even then, you still have another version
of the problem, because now imagine you have one *table* that contains
nothing but the value 'a' and another that contains nothing but the
entire text of Don Quixote, it is going to be possible for an
adversary to tell those tables apart, even with the corresponding
files encrypted in their entirety.

But I don't think that this means that a feature like this has no
value. I think it just means that we need to clearly document how the
feature works and not over-promise.

--
Robert Haas
EDB: http://www.enterprisedb.com

#22Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#18)
1 attachment(s)
Re: Transparent column encryption

On 12.07.22 20:29, Peter Eisentraut wrote:

Updated patch, to resolve some merge conflicts.

Rebased patch, no new functionality

Attachments:

v5-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v5-0001-Transparent-column-encryption.patchDownload
From 018248c37e0a37ca787bfd09eb3da94a7a9dab2a Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Tue, 19 Jul 2022 15:46:14 +0200
Subject: [PATCH v5] 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 the "prying eyes" of DBAs, sysadmins, cloud operators,
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.  Of course, you
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 you can do equality lookups, at the cost
of some security.

This functionality also exists in other SQL database products, so the
overall concepts weren't invented by me by any means.

Also, this feature has nothing to do with the on-disk encryption
feature being contemplated in parallel.  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.

TODO: Some protocol extensions are required.  These should be guarded
by some _pq_... setting, but this is not done in this patch yet.  As
mentioned above, extra messages are added for sending the CMKs and
CEKs.  In the RowDescription message, I have commandeered the format
field to add a bit that indicates that the field is encrypted.  This
could be made a separate field, and there should probably be
additional fields to indicate the algorithm and CEK name, but this was
easiest for now.  The ParameterDescription message is extended to
contain format fields for each parameter, for the same purpose.
Again, this could be done differently.

Speaking of parameter descriptions, 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:

- additional DDL support
- protocol versioning
- forward and backward compatibility
- pg_dump support
- psql \d support

Discussion: https://www.postgresql.org/message-id/flat/89157929-c2b6-817b-6025-8e4b2d89d88f%40enterprisedb.com
---
 .../postgres_fdw/expected/postgres_fdw.out    |   2 +-
 doc/src/sgml/acronyms.sgml                    |  18 +
 doc/src/sgml/catalogs.sgml                    | 275 +++++++
 doc/src/sgml/datatype.sgml                    |  47 ++
 doc/src/sgml/ddl.sgml                         | 310 +++++++
 doc/src/sgml/glossary.sgml                    |  23 +
 doc/src/sgml/libpq.sgml                       | 257 +++++-
 doc/src/sgml/protocol.sgml                    | 346 ++++++++
 doc/src/sgml/ref/allfiles.sgml                |   2 +
 .../ref/create_column_encryption_key.sgml     | 144 ++++
 .../sgml/ref/create_column_master_key.sgml    | 108 +++
 doc/src/sgml/ref/create_table.sgml            |  42 +-
 doc/src/sgml/ref/psql-ref.sgml                |  33 +
 doc/src/sgml/reference.sgml                   |   2 +
 src/backend/access/common/printsimple.c       |   2 +
 src/backend/access/common/printtup.c          | 162 ++++
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |   4 +
 src/backend/catalog/heap.c                    |   3 +
 src/backend/catalog/objectaddress.c           |   2 +
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/colenccmds.c             | 245 ++++++
 src/backend/commands/event_trigger.c          |   6 +
 src/backend/commands/prepare.c                |  74 +-
 src/backend/commands/seclabel.c               |   2 +
 src/backend/commands/tablecmds.c              |  74 ++
 src/backend/executor/spi.c                    |   4 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/analyze.c                  |   3 +-
 src/backend/parser/gram.y                     |  43 +-
 src/backend/parser/parse_param.c              |  43 +-
 src/backend/parser/parse_target.c             |   6 +
 src/backend/tcop/postgres.c                   |  54 +-
 src/backend/tcop/utility.c                    |  15 +
 src/backend/utils/adt/arrayfuncs.c            |   1 +
 src/backend/utils/cache/plancache.c           |  10 +
 src/backend/utils/cache/syscache.c            |  48 ++
 src/bin/pg_dump/pg_dump.c                     |  26 +-
 src/bin/pg_dump/pg_dump.h                     |   1 +
 src/bin/psql/command.c                        |  31 +
 src/bin/psql/common.c                         |  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                   |   2 +-
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |   9 +
 src/include/catalog/pg_cast.dat               |   6 +
 src/include/catalog/pg_colenckey.h            |  55 ++
 src/include/catalog/pg_colenckeydata.h        |  46 ++
 src/include/catalog/pg_colmasterkey.h         |  45 ++
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  10 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               |  13 +-
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  24 +
 src/include/nodes/parsenodes.h                |   3 +
 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                 |   2 +
 src/include/tcop/tcopprot.h                   |   2 +
 src/include/utils/plancache.h                 |   6 +
 src/include/utils/syscache.h                  |   5 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  20 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 758 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 594 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 123 ++-
 src/interfaces/libpq/fe-trace.c               |   7 +
 src/interfaces/libpq/libpq-fe.h               |  22 +
 src/interfaces/libpq/libpq-int.h              |  34 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/test/Makefile                             |   3 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  29 +
 .../t/001_column_encryption.pl                | 180 +++++
 .../column_encryption/t/002_cmk_rotation.pl   | 118 +++
 src/test/column_encryption/test_client.c      | 210 +++++
 .../column_encryption/test_run_decrypt.pl     |  41 +
 .../traces/disallowed_in_pipeline.trace       |   2 +-
 .../traces/multi_pipelines.trace              |   4 +-
 .../libpq_pipeline/traces/nosync.trace        |  20 +-
 .../traces/pipeline_abort.trace               |   4 +-
 .../libpq_pipeline/traces/pipeline_idle.trace |  16 +-
 .../libpq_pipeline/traces/prepared.trace      |   6 +-
 .../traces/simple_pipeline.trace              |   2 +-
 .../libpq_pipeline/traces/singlerow.trace     |   6 +-
 .../libpq_pipeline/traces/transaction.trace   |   2 +-
 .../regress/expected/column_encryption.out    |  37 +
 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     |  20 +-
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    |  28 +
 src/test/regress/sql/prepare.sql              |   2 +-
 src/test/regress/sql/psql.sql                 |  17 +
 src/test/regress/sql/type_sanity.sql          |   4 +-
 114 files changed, 5150 insertions(+), 142 deletions(-)
 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 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/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/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index ebf9ea3598..d25f9e263d 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9575,7 +9575,7 @@ DO $d$
     END;
 $d$;
 ERROR:  invalid option "password"
-HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
+HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, cmklookup, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
 CONTEXT:  SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
 PL/pgSQL function inline_code_block line 3 at EXECUTE
 -- If we add a password for our user mapping instead, we should get a different
diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 9ed148ab84..e21c9bcce3 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -56,6 +56,15 @@ <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 670a5406d6..124e84a7c1 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>
@@ -2437,6 +2486,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 8e30b82273..cdd0530eeb 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5342,4 +5342,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 b01e3ad544..607b4cd790 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1211,6 +1211,316 @@ <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>
+    </listitem>
+   </orderedlist>
+  </para>
+  </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 d6d0a3a814..10507e4391 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 74456aa69d..310aa49e37 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1974,6 +1974,110 @@ <title>Parameter Key Words</title>
        </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>
@@ -3022,6 +3126,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>
 
@@ -3872,6 +4027,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>
 
@@ -4047,12 +4224,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>
 
@@ -4578,6 +4780,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
@@ -4585,6 +4788,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.
@@ -4696,6 +4900,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>
 
@@ -4746,6 +4990,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"/>
@@ -7776,6 +8021,16 @@ <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>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index c0b89a3c01..60595c33b1 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1050,6 +1050,52 @@ <title>Extended Query</title>
    </note>
   </sect2>
 
+  <sect2>
+   <title>Transparent Column Encryption</title>
+
+   <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>
+    The cryptographic operations used for transparent column encryption are
+    described in <xref linkend="protocol-transparent-column-encryption"/>.
+   </para>
+  </sect2>
+
   <sect2>
    <title>Function Call</title>
 
@@ -3991,6 +4037,128 @@ <title>Message Formats</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="protocol-message-formats-ColumnEncryptionKey">
+    <term>ColumnEncryptionKey (B)</term>
+    <listitem>
+     <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>
+     <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>
@@ -5086,6 +5254,37 @@ <title>Message Formats</title>
         </para>
        </listitem>
       </varlistentry>
+
+      <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>
@@ -5474,6 +5673,26 @@ <title>Message Formats</title>
         </para>
        </listitem>
       </varlistentry>
+
+      <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 encrypt algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
      </variablelist>
     </listitem>
    </varlistentry>
@@ -7278,6 +7497,133 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-transparent-column-encryption">
+  <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 e90a0e1f83..9ae56ee950 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -62,6 +62,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">
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 0000000000..e7965ebcca
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,144 @@
+<!--
+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>
+ </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>
+ </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">
+  <!-- TODO
+   <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 0000000000..db3043d715
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,108 @@
+<!--
+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">
+  <!-- TODO
+   <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 6bbf15ed1a..28b49d093c 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/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 65bb0a6a3f..e6c93f1b13 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2255,6 +2255,28 @@ <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>
 
@@ -3945,6 +3967,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 a3b743e8c1..2e37500031 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -90,6 +90,8 @@ <title>SQL Commands</title>
    &createAggregate;
    &createCast;
    &createCollation;
+   &createColumnEncryptionKey;
+   &createColumnMasterKey;
    &createConversion;
    &createDatabase;
    &createDomain;
diff --git a/src/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index c99ae54cb0..9d3476814f 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -46,6 +46,8 @@ printsimple_startup(DestReceiver *self, int operation, TupleDesc tupdesc)
 		pq_sendint16(&buf, attr->attlen);
 		pq_sendint32(&buf, attr->atttypmod);
 		pq_sendint16(&buf, 0);	/* format code */
+		pq_sendint32(&buf, 0);	/* CEK */
+		pq_sendint16(&buf, 0);	/* CEK alg */
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index d2f3b57288..28b437e5c7 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,13 +15,26 @@
  */
 #include "postgres.h"
 
+#include "access/genam.h"
 #include "access/printtup.h"
+#include "access/skey.h"
+#include "access/table.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
 #include "libpq/libpq.h"
 #include "libpq/pqformat.h"
 #include "tcop/pquery.h"
+#include "utils/array.h"
+#include "utils/arrayaccess.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/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 +164,135 @@ 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;
+
+	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;
+
+	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
  *
@@ -200,6 +342,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 +375,22 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		else
 			format = 0;
 
+		if (get_typtype(atttypid) == TYPTYPE_ENCRYPTED)
+		{
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(resorigtbl), Int16GetDatum(resorigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", resorigcol, resorigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			MaybeSendColumnEncryptionKeyMessage(orig_att->attcek);
+			atttypid = orig_att->attrealtypid;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->attencalg;
+			ReleaseSysCache(tp);
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +398,8 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		pq_writeint16(buf, att->attlen);
 		pq_writeint32(buf, atttypmod);
 		pq_writeint16(buf, format);
+		pq_writeint32(buf, attcekid);
+		pq_writeint16(buf, attencalg);
 	}
 
 	pq_endmessage_reuse(buf);
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index d6fb261e20..e3ecc64ea7 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attrealtypid != attr2->attrealtypid)
+			return false;
+		if (attr1->attencalg != attr2->attencalg)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 10bf26ce7c..cea91d4a88 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 89a0221ec9..7e1a91b974 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 17ff617fba..38d7e78176 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3579,6 +3579,8 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEK:
+					case OBJECT_CMK:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3719,6 +3721,8 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEK:
+					case OBJECT_CMK:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 9b03579e6e..e91b0b806d 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -746,6 +746,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attrealtypid - 1] = ObjectIdGetDatum(attrs->attrealtypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attencalg - 1] = Int16GetDatum(attrs->attencalg);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 6080ff8f5f..598b18f3cc 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -2303,6 +2303,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:
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index 48f7348f91..69f6175c60 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/colenccmds.c b/src/backend/commands/colenccmds.c
new file mode 100644
index 0000000000..205733bdfc
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,245 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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/rel.h"
+#include "utils/syscache.h"
+
+ObjectAddress
+CreateCEK(ParseState *pstate, DefineStmt *stmt)
+{
+	AclResult	aclresult;
+	Relation	rel;
+	Relation	rel2;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = 0;
+	int16		alg;
+	char	   *encval;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	Datum		values2[Natts_pg_colenckeydata] = {0};
+	bool		nulls2[Natts_pg_colenckeydata] = {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);
+	rel2 = table_open(ColumnEncKeyDataRelationId, 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)))));
+
+	foreach(lc, stmt->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 (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)
+		encval = defGetString(encvalEl);
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("attribute \"%s\" must be specified",
+						"encrypted_value")));
+
+	/* pg_colenckey */
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+	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);
+
+	/* pg_colenckeydata */
+	values2[Anum_pg_colenckeydata_oid - 1] =
+		ObjectIdGetDatum(GetNewOidWithIndex(rel, ColumnEncKeyDataOidIndexId, Anum_pg_colenckeydata_oid));
+	values2[Anum_pg_colenckeydata_ckdcekid - 1] = ObjectIdGetDatum(cekoid);
+	values2[Anum_pg_colenckeydata_ckdcmkid - 1] = ObjectIdGetDatum(cmkoid);
+	values2[Anum_pg_colenckeydata_ckdcmkalg - 1] = Int16GetDatum(alg);
+	values2[Anum_pg_colenckeydata_ckdencval - 1] = DirectFunctionCall1(byteain, CStringGetDatum(encval));
+
+	tup = heap_form_tuple(RelationGetDescr(rel2), values2, nulls2);
+	CatalogTupleInsert(rel2, tup);
+	heap_freetuple(tup);
+
+	recordDependencyOnOwner(ColumnEncKeyRelationId, cekoid, GetUserId());
+
+	ObjectAddressSet(myself, ColumnEncKeyRelationId, cekoid);
+
+	table_close(rel2, RowExclusiveLock);
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnMasterKeyRelationId, cekoid, 0);
+
+	return myself;
+}
+
+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 eef3e5d56e..0bfb150ec2 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,8 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -2055,6 +2057,8 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2138,6 +2142,8 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 579825c159..0f2ed67a96 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -50,7 +50,6 @@ static void InitQueryHashTable(void);
 static ParamListInfo EvaluateParams(ParseState *pstate,
 									PreparedStatement *pstmt, List *params,
 									EState *estate);
-static Datum build_regtype_array(Oid *param_types, int num_params);
 
 /*
  * Implements the 'PREPARE' utility statement.
@@ -62,6 +61,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	RawStmt    *rawstmt;
 	CachedPlanSource *plansource;
 	Oid		   *argtypes = NULL;
+	Oid		   *argorigtbls = NULL;
+	AttrNumber *argorigcols = NULL;
 	int			nargs;
 	List	   *query_list;
 
@@ -108,6 +109,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 
 			argtypes[i++] = toid;
 		}
+
+		argorigtbls = (Oid *) palloc0(nargs * sizeof(Oid));
+		argorigcols = (AttrNumber *) palloc0(nargs * sizeof(AttrNumber));
 	}
 
 	/*
@@ -117,7 +121,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
 	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
-												  &argtypes, &nargs, NULL);
+												  &argtypes, &nargs,
+												  &argorigtbls, &argorigcols,
+												  NULL);
 
 	/* Finish filling in the CachedPlanSource */
 	CompleteCachedPlan(plansource,
@@ -125,6 +131,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 					   NULL,
 					   argtypes,
 					   nargs,
+					   argorigtbls,
+					   argorigcols,
 					   NULL,
 					   NULL,
 					   CURSOR_OPT_PARALLEL_OK,	/* allow parallel mode */
@@ -683,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 = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_types[i]);
+			values[3] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGTYPEOID));
+
+			tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_origtbls[i]);
+			values[4] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGCLASSOID));
+
+			tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = Int16GetDatum(prep_stmt->plansource->param_origcols[i]);
+			values[5] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, INT2OID));
+
 			if (result_desc)
 			{
-				Oid		   *result_types;
-
-				result_types = (Oid *) palloc(result_desc->natts * sizeof(Oid));
+				tmp_ary = (Datum *) palloc(result_desc->natts * sizeof(Datum));
 				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 = (Datum *) palloc(num_params * sizeof(Datum));
-
-	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 7ae19b98bb..9ccdf7616c 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,8 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		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 a2f577024a..bb813d4f7f 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"
@@ -933,6 +934,76 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			ListCell   *lc;
+			char	   *cek = NULL;
+			Oid			cekoid;
+			bool		encdet = false;
+			int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+			foreach(lc, colDef->encryption)
+			{
+				DefElem    *el = lfirst_node(DefElem, lc);
+
+				if (strcmp(el->defname, "column_encryption_key") == 0)
+					cek = strVal(linitial(castNode(TypeName, el->arg)->names));
+				else if (strcmp(el->defname, "encryption_type") == 0)
+				{
+					char	   *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+					if (strcmp(val, "deterministic") == 0)
+						encdet = true;
+					else if (strcmp(val, "randomized") == 0)
+						encdet = false;
+					else
+						ereport(ERROR,
+								errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								errmsg("unrecognized encryption type: %s", val));
+				}
+				else if (strcmp(el->defname, "algorithm") == 0)
+				{
+					char	   *val = strVal(el->arg);
+
+					if (strcmp(val, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+						alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+					else if (strcmp(val, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+						alg = PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+					else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+						alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+					else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+						alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+					else
+						ereport(ERROR,
+								errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								errmsg("unrecognized encryption algorithm: %s", val));
+				}
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("unrecognized column encryption parameter: %s", el->defname));
+			}
+
+			if (!cek)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+						errmsg("column encryption key must be specified"));
+
+			cekoid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid, PointerGetDatum(cek));
+			if (!cekoid)
+				ereport(ERROR,
+						errcode(ERRCODE_UNDEFINED_OBJECT),
+						errmsg("column encryption key \"%s\" does not exist", cek));
+
+			attr->attcek = cekoid;
+			attr->attrealtypid = attr->atttypid;
+			if (encdet)
+				attr->atttypid = PG_ENCRYPTED_DETOID;
+			else
+				attr->atttypid = PG_ENCRYPTED_RNDOID;
+			attr->attencalg = alg;
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -6837,6 +6908,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attgenerated = colDef->generated;
 	attribute.attisdropped = false;
 	attribute.attislocal = colDef->is_local;
+	attribute.attcek = 0; // TODO
+	attribute.attrealtypid = 0; // TODO
+	attribute.attencalg = 0; // TODO
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
 
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 29bc26669b..cde5b0e087 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2279,6 +2279,8 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan)
 						   NULL,
 						   plan->argtypes,
 						   plan->nargs,
+						   NULL,
+						   NULL,
 						   plan->parserSetup,
 						   plan->parserSetupArg,
 						   plan->cursor_options,
@@ -2516,6 +2518,8 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 							   NULL,
 							   plan->argtypes,
 							   plan->nargs,
+							   NULL,
+							   NULL,
 							   plan->parserSetup,
 							   plan->parserSetupArg,
 							   plan->cursor_options,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 4cb1744da6..8fbe0f5f8f 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4288,6 +4288,8 @@ raw_expression_tree_walker(Node *node,
 					return true;
 				if (walker(coldef->compression, context))
 					return true;
+				if (walker(coldef->encryption, context))
+					return true;
 				if (walker(coldef->raw_default, context))
 					return true;
 				if (walker(coldef->collClause, context))
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 8ed2c4b8c7..6e1b24bd4b 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 df5ceea910..0bbf54f623 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -596,6 +596,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <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
@@ -789,7 +790,7 @@ 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 EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
+	EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTION ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
 	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
@@ -814,7 +815,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 NESTED NEW NEXT NFC NFD NFKC NFKD NO
@@ -3778,14 +3779,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;
@@ -3794,8 +3796,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;
@@ -3852,6 +3854,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 		;
@@ -6345,6 +6352,24 @@ DefineStmt:
 					n->if_not_exists = true;
 					$$ = (Node *) n;
 				}
+			| CREATE COLUMN ENCRYPTION KEY any_name WITH VALUES definition
+				{
+					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; }
@@ -17782,6 +17807,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ERROR_P
 			| ESCAPE
@@ -17849,6 +17875,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -18353,6 +18380,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ERROR_P
@@ -18456,6 +18484,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 f668abfcb3..b45bf4c438 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -49,6 +49,8 @@ typedef struct VarParamState
 {
 	Oid		  **paramTypes;		/* array of parameter type OIDs */
 	int		   *numParams;		/* number of array entries */
+	Oid		  **paramOrigTbls;	/* underlying tables (0 if none) */
+	AttrNumber **paramOrigCols; /* underlying columns (0 if none) */
 } VarParamState;
 
 static Node *fixed_paramref_hook(ParseState *pstate, ParamRef *pref);
@@ -56,6 +58,7 @@ static Node *variable_paramref_hook(ParseState *pstate, ParamRef *pref);
 static Node *variable_coerce_param_hook(ParseState *pstate, Param *param,
 										Oid targetTypeId, int32 targetTypeMod,
 										int location);
+static void variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 static bool check_parameter_resolution_walker(Node *node, ParseState *pstate);
 static bool query_contains_extern_params_walker(Node *node, void *context);
 
@@ -81,15 +84,19 @@ setup_parse_fixed_parameters(ParseState *pstate,
  */
 void
 setup_parse_variable_parameters(ParseState *pstate,
-								Oid **paramTypes, int *numParams)
+								Oid **paramTypes, int *numParams,
+								Oid **paramOrigTbls, AttrNumber **paramOrigCols)
 {
 	VarParamState *parstate = palloc(sizeof(VarParamState));
 
 	parstate->paramTypes = paramTypes;
 	parstate->numParams = numParams;
+	parstate->paramOrigTbls = paramOrigTbls;
+	parstate->paramOrigCols = paramOrigCols;
 	pstate->p_ref_hook_state = (void *) parstate;
 	pstate->p_paramref_hook = variable_paramref_hook;
 	pstate->p_coerce_param_hook = variable_coerce_param_hook;
+	pstate->p_param_assign_orig_hook = variable_param_assign_orig_hook;
 }
 
 /*
@@ -145,14 +152,36 @@ variable_paramref_hook(ParseState *pstate, ParamRef *pref)
 	{
 		/* Need to enlarge param array */
 		if (*parstate->paramTypes)
+		{
 			*parstate->paramTypes = (Oid *) repalloc(*parstate->paramTypes,
 													 paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc(*parstate->paramOrigTbls,
+													paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc(*parstate->paramOrigCols,
+													paramno * sizeof(AttrNumber));
+		}
 		else
+		{
 			*parstate->paramTypes = (Oid *) palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc(paramno * sizeof(AttrNumber));
+		}
 		/* Zero out the previously-unreferenced slots */
 		MemSet(*parstate->paramTypes + *parstate->numParams,
 			   0,
 			   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigTbls)
+			MemSet(*parstate->paramOrigTbls + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigCols)
+			MemSet(*parstate->paramOrigCols + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(AttrNumber));
 		*parstate->numParams = paramno;
 	}
 
@@ -260,6 +289,18 @@ variable_coerce_param_hook(ParseState *pstate, Param *param,
 	return NULL;
 }
 
+static void
+variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol)
+{
+	VarParamState *parstate = (VarParamState *) pstate->p_ref_hook_state;
+	int			paramno = param->paramid;
+
+	if (parstate->paramOrigTbls)
+		(*parstate->paramOrigTbls)[paramno - 1] = origtbl;
+	if (parstate->paramOrigCols)
+		(*parstate->paramOrigCols)[paramno - 1] = origcol;
+}
+
 /*
  * Check for consistent assignment of variable parameters after completion
  * of parsing with parse_variable_parameters.
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 16a0fe59e2..3534683cb0 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -595,6 +595,12 @@ transformAssignedExpr(ParseState *pstate,
 					 parser_errposition(pstate, exprLocation(orig_expr))));
 	}
 
+	if (IsA(expr, Param))
+	{
+		if (pstate->p_param_assign_orig_hook)
+			pstate->p_param_assign_orig_hook(pstate, castNode(Param, expr), RelationGetRelid(pstate->p_target_relation), attrno);
+	}
+
 	pstate->p_expr_kind = sv_expr_kind;
 
 	return expr;
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 6f18b68856..49078a733a 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -79,6 +79,7 @@
 #include "utils/memutils.h"
 #include "utils/ps_status.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
 
@@ -673,6 +674,8 @@ pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 								 const char *query_string,
 								 Oid **paramTypes,
 								 int *numParams,
+								 Oid **paramOrigTbls,
+								 AttrNumber **paramOrigCols,
 								 QueryEnvironment *queryEnv)
 {
 	Query	   *query;
@@ -687,7 +690,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.
@@ -1364,6 +1367,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	bool		is_named;
 	bool		save_log_statement_stats = log_statement_stats;
 	char		msec_str[32];
+	Oid		   *paramOrigTbls = palloc(numParams * sizeof(Oid));
+	AttrNumber *paramOrigCols = palloc(numParams * sizeof(AttrNumber));
 
 	/*
 	 * Report query to various monitoring facilities.
@@ -1484,6 +1489,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 */
@@ -1514,6 +1521,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 */
@@ -1811,6 +1820,16 @@ exec_bind_message(StringInfo input_message)
 			else
 				pformat = 0;	/* default = text */
 
+			if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+			{
+				if (pformat & 0xF0)
+					pformat &= ~0xF0;
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_PROTOCOL_VIOLATION),
+							 errmsg("parameter corresponds to an encrypted column, but the parameter value was not encrypted")));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2607,8 +2626,41 @@ exec_describe_statement_message(const char *stmt_name)
 	for (int i = 0; i < psrc->num_params; i++)
 	{
 		Oid			ptype = psrc->param_types[i];
+		Oid			pcekid = InvalidOid;
+		int			pcekalg = 0;
+		int16		pflags = 0;
+
+		if (get_typtype(ptype) == TYPTYPE_ENCRYPTED)
+		{
+			Oid			porigtbl = psrc->param_origtbls[i];
+			AttrNumber	porigcol = psrc->param_origcols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol <= 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("parameter %d corresponds to an encrypted column, but an underlying table and column could not be determined", i)));
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(porigtbl), Int16GetDatum(porigcol));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", porigcol, porigtbl);
+			orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+			ptype = orig_att->attrealtypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->attencalg;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x01;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		pq_sendint32(&row_description_buf, (int) pcekid);
+		pq_sendint16(&row_description_buf, pcekalg);
+		pq_sendint16(&row_description_buf, pflags);
 	}
 	pq_endmessage_reuse(&row_description_buf);
 
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 6b0a865262..0880796cf9 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"
@@ -1441,6 +1442,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,
@@ -2760,6 +2769,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;
diff --git a/src/backend/utils/adt/arrayfuncs.c b/src/backend/utils/adt/arrayfuncs.c
index fb167f226a..6dd2a3b383 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/plancache.c b/src/backend/utils/cache/plancache.c
index 0d6a295674..f6893dd8b7 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -340,6 +340,8 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 				   MemoryContext querytree_context,
 				   Oid *param_types,
 				   int num_params,
+				   Oid *param_origtbls,
+				   AttrNumber *param_origcols,
 				   ParserSetupHook parserSetup,
 				   void *parserSetupArg,
 				   int cursor_options,
@@ -419,6 +421,14 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 	{
 		plansource->param_types = (Oid *) palloc(num_params * sizeof(Oid));
 		memcpy(plansource->param_types, param_types, num_params * sizeof(Oid));
+
+		plansource->param_origtbls = (Oid *) palloc0(num_params * sizeof(Oid));
+		if (param_origtbls)
+			memcpy(plansource->param_origtbls, param_origtbls, num_params * sizeof(Oid));
+
+		plansource->param_origcols = (AttrNumber *) palloc0(num_params * sizeof(AttrNumber));
+		if (param_origcols)
+			memcpy(plansource->param_origcols, param_origcols, num_params * sizeof(AttrNumber));
 	}
 	else
 		plansource->param_types = NULL;
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 1912b12146..9a27198087 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,33 @@ static const struct cachedesc cacheinfo[] = {
 		},
 		256
 	},
+	{
+		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 +319,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/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e4fdb6b75b..a4ab76c847 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8073,6 +8073,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcekname;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8182,17 +8183,24 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	if (fout->remoteVersion >= 120000)
 		appendPQExpBufferStr(q,
-							 "a.attgenerated\n");
+							 "a.attgenerated,\n");
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "'' AS attgenerated,\n");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBufferStr(q,
+							 "(SELECT cekname FROM pg_colenckey cek WHERE cek.oid = a.attcek ) AS attcekname\n");
+	else
+		appendPQExpBufferStr(q,
+							 "NULL AS attcekname\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
 					  "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
 					  "JOIN pg_catalog.pg_attribute a ON (src.tbloid = a.attrelid) "
 					  "LEFT JOIN pg_catalog.pg_type t "
-					  "ON (a.atttypid = t.oid)\n"
+					  "ON (CASE WHEN a.attrealtypid <> 0 THEN a.attrealtypid ELSE a.atttypid END = t.oid)\n"
 					  "WHERE a.attnum > 0::pg_catalog.int2\n"
 					  "ORDER BY a.attrelid, a.attnum",
 					  tbloids->data);
@@ -8211,6 +8219,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcekname = PQfnumber(res, "attcekname");
 	i_attisdropped = PQfnumber(res, "attisdropped");
 	i_attlen = PQfnumber(res, "attlen");
 	i_attalign = PQfnumber(res, "attalign");
@@ -8272,6 +8281,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		tbinfo->typstorage = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attidentity = (char *) pg_malloc(numatts * sizeof(char));
 		tbinfo->attgenerated = (char *) pg_malloc(numatts * sizeof(char));
+		tbinfo->attcekname = (char **) pg_malloc(numatts * sizeof(char *));
 		tbinfo->attisdropped = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attlen = (int *) pg_malloc(numatts * sizeof(int));
 		tbinfo->attalign = (char *) pg_malloc(numatts * sizeof(char));
@@ -8300,6 +8310,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->attidentity[j] = *(PQgetvalue(res, r, i_attidentity));
 			tbinfo->attgenerated[j] = *(PQgetvalue(res, r, i_attgenerated));
 			tbinfo->needs_override = tbinfo->needs_override || (tbinfo->attidentity[j] == ATTRIBUTE_IDENTITY_ALWAYS);
+			if (!PQgetisnull(res, r, i_attcekname))
+				tbinfo->attcekname[j] = pg_strdup(PQgetvalue(res, r, i_attcekname));
+			else
+				tbinfo->attcekname[j] = NULL;
 			tbinfo->attisdropped[j] = (PQgetvalue(res, r, i_attisdropped)[0] == 't');
 			tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
 			tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
@@ -15267,6 +15281,12 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcekname[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s)",
+										  fmtId(tbinfo->attcekname[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 1d21c2906f..2c176ed13c 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -333,6 +333,7 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	char	  **attcekname;
 	int		   *attlen;			/* attribute length, used by binary_upgrade */
 	char	   *attalign;		/* attribute align, used by binary_upgrade */
 	bool	   *attislocal;		/* true if attr has local definition */
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 0955142215..663788ef64 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 9f95869eca..8fe055ffed 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1284,6 +1284,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;
 
@@ -1448,7 +1456,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 88d92a08ae..fe81547b29 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1498,7 +1498,7 @@ describeOneTableDetails(const char *schemaname,
 	bool		printTableInitialized = false;
 	int			i;
 	char	   *view_def = NULL;
-	char	   *headers[12];
+	char	   *headers[13];
 	PQExpBufferData title;
 	PQExpBufferData tmpbuf;
 	int			cols;
@@ -1514,6 +1514,7 @@ describeOneTableDetails(const char *schemaname,
 				fdwopts_col = -1,
 				attstorage_col = -1,
 				attcompression_col = -1,
+				attcekname_col = -1,
 				attstattarget_col = -1,
 				attdescr_col = -1;
 	int			numrows;
@@ -1812,7 +1813,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)
@@ -1826,7 +1827,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");
@@ -1877,6 +1878,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 ||
@@ -2000,6 +2010,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)
@@ -2092,6 +2104,17 @@ describeOneTableDetails(const char *schemaname,
 							  false, false);
 		}
 
+		/* Column encryption */
+		if (attcekname_col >= 0)
+		{
+			if (!PQgetisnull(res, i, attcekname_col))
+				printTableAddCell(&cont, PQgetvalue(res, i, attcekname_col),
+								  false, false);
+			else
+				printTableAddCell(&cont, "",
+								  false, false);
+		}
+
 		/* Statistics target, if the relkind supports this feature */
 		if (attstattarget_col >= 0)
 			printTableAddCell(&cont, PQgetvalue(res, i, attstattarget_col),
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index f8ce1a0706..9f4fce26fd 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -195,6 +195,7 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\g [(OPTIONS)] [FILE]  execute query (and send result to file or |pipe);\n"
 		  "                         \\g with no arguments is equivalent to a semicolon\n");
 	HELP0("  \\gdesc                 describe result of query, without executing it\n");
+	HELP0("  \\gencr [PARAM]...      execute query, with support for parameter encryption\n");
 	HELP0("  \\gexec                 execute query, then execute each value in its result\n");
 	HELP0("  \\gset [PREFIX]         execute query and store result in psql variables\n");
 	HELP0("  \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n");
@@ -412,6 +413,8 @@ helpVariables(unsigned short int pager)
 		  "    true if last query failed, else false\n");
 	HELP0("  FETCH_COUNT\n"
 		  "    the number of result rows to fetch and display at a time (0 = unlimited)\n");
+	HELP0("  HIDE_COLUMN_ENCRYPTION\n"
+		  "    if set, column encryption details are not displayed\n");
 	HELP0("  HIDE_TABLEAM\n"
 		  "    if set, table access methods are not displayed\n");
 	HELP0("  HIDE_TOAST_COMPRESSION\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 2399cffa3f..636729df1d 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -95,6 +95,10 @@ typedef struct _psqlSettings
 
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
 	bool		gdesc_flag;		/* one-shot request to describe query result */
+	bool		gencr_flag;		/* one-shot request to send query with support
+								 * for parameter encryption */
+	int			num_params;		/* number of query parameters */
+	char	  **params;			/* query parameters */
 	bool		gexec_flag;		/* one-shot request to execute query result */
 	bool		crosstab_flag;	/* one-shot request to crosstab result */
 	char	   *ctv_args[4];	/* \crosstabview arguments */
@@ -134,6 +138,7 @@ typedef struct _psqlSettings
 	bool		quiet;
 	bool		singleline;
 	bool		singlestep;
+	bool		hide_column_encryption;
 	bool		hide_compression;
 	bool		hide_tableam;
 	int			fetch_count;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 7c2f555f15..c955222a50 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -1188,6 +1188,13 @@ hide_compression_hook(const char *newval)
 							 &pset.hide_compression);
 }
 
+static bool
+hide_column_encryption_hook(const char *newval)
+{
+	return ParseVariableBool(newval, "HIDE_COLUMN_ENCRYPTION",
+							 &pset.hide_column_encryption);
+}
+
 static bool
 hide_tableam_hook(const char *newval)
 {
@@ -1259,6 +1266,9 @@ EstablishVariableSpace(void)
 	SetVariableHooks(pset.vars, "SHOW_CONTEXT",
 					 show_context_substitute_hook,
 					 show_context_hook);
+	SetVariableHooks(pset.vars, "HIDE_COLUMN_ENCRYPTION",
+					 bool_substitute_hook,
+					 hide_column_encryption_hook);
 	SetVariableHooks(pset.vars, "HIDE_TOAST_COMPRESSION",
 					 bool_substitute_hook,
 					 hide_compression_hook);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index e572f585ef..02e8a3512b 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1692,7 +1692,7 @@ psql_completion(const char *text, int start, int end)
 		"\\echo", "\\edit", "\\ef", "\\elif", "\\else", "\\encoding",
 		"\\endif", "\\errverbose", "\\ev",
 		"\\f",
-		"\\g", "\\gdesc", "\\getenv", "\\gexec", "\\gset", "\\gx",
+		"\\g", "\\gdesc", "\\gencr", "\\getenv", "\\gexec", "\\gset", "\\gx",
 		"\\help", "\\html",
 		"\\if", "\\include", "\\include_relative", "\\ir",
 		"\\list", "\\lo_import", "\\lo_export", "\\lo_list", "\\lo_unlink",
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 971a74cf22..db1f3b8811 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,6 +20,8 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 61cd591430..a0bd708132 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4cc129bebd..dddb27113f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 053294c99f..550a53649b 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,15 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	bool		attislocal BKI_DEFAULT(t);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey);
+
+	/* real type if encrypted */
+	Oid			attrealtypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+
+	/* encryption algorithm (PG_CEK_* values) */
+	int16		attencalg BKI_DEFAULT(0);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index 4471eb6bbe..878b0bcbbf 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -546,4 +546,10 @@
 { castsource => 'tstzrange', casttarget => 'tstzmultirange',
   castfunc => 'tstzmultirange(tstzrange)', castcontext => 'e',
   castmethod => 'f' },
+
+{ castsource => 'bytea', casttarget => 'pg_encrypted_det', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+{ castsource => 'bytea', casttarget => 'pg_encrypted_rnd', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+
 ]
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..095ed712b0
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,55 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+/* ----------------
+ *		pg_colenckey definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckey
+ * ----------------
+ */
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_index, 8242, ColumnEncKeyNameIndexId, on pg_colenckey using btree(cekname name_ops));
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  For clarity, the assigned numbers are not reused between CMKs
+ * and CEKs, but that is not technically required.  In either case, don't
+ * assign zero, so that that can be used as an invalid value.
+ */
+
+#define PG_CMK_RSAES_OAEP_SHA_1			1
+#define PG_CMK_RSAES_OAEP_SHA_256		2
+
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	130
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	131
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	132
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	133
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..3e4dea7218
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkeydata.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+/* ----------------
+ *		pg_colenckeydata definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckeydata
+ * ----------------
+ */
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+	int16		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..0344cc4201
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,45 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_index, 8241, ColumnMasterKeyNameIndexId, on pg_colmasterkey using btree(cmkname name_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index dbcae7ffdd..0ca401ffe4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index bc5f8213f3..4737a7f9ed 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index b3b6a7e616..5343580b06 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2e41f4d9e8..2fa26a2222 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8025,9 +8025,9 @@
   proname => 'pg_prepared_statement', prorows => '1000', proretset => 't',
   provolatile => 's', proparallel => 'r', prorettype => 'record',
   proargtypes => '',
-  proallargtypes => '{text,text,timestamptz,_regtype,_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',
@@ -11886,4 +11886,11 @@
   prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary',
   prosrc => 'brin_minmax_multi_summary_send' },
 
+{ oid => '8245',
+  proname => 'pg_encrypted_det_eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'pg_encrypted_det_ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index df45879463..df1fa3e393 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,16 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'y',
+  typcategory => 'Y', typinput => 'byteain', typoutput => 'byteaout',
+  typreceive => 'bytearecv', typsend => 'byteasend', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 48a2559137..11aff9ff83 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -277,6 +277,7 @@ DECLARE_UNIQUE_INDEX(pg_type_typname_nsp_index, 2704, TypeNameNspIndexId, on pg_
 #define  TYPTYPE_MULTIRANGE	'm' /* multirange type */
 #define  TYPTYPE_PSEUDO		'p' /* pseudo-type */
 #define  TYPTYPE_RANGE		'r' /* range type */
+#define  TYPTYPE_ENCRYPTED	'y' /* encrypted column value */
 
 #define  TYPCATEGORY_INVALID	'\0'	/* not an allowed category */
 #define  TYPCATEGORY_ARRAY		'A'
diff --git a/src/include/commands/colenccmds.h b/src/include/commands/colenccmds.h
new file mode 100644
index 0000000000..1019182925
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,24 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 CreateCMK(ParseState *pstate, DefineStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 98fe1abaa2..2a47d81d92 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? */
@@ -2151,6 +2152,8 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index dc379547c7..c310e124f1 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -28,7 +28,9 @@ extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook;
 extern Query *parse_analyze_fixedparams(RawStmt *parseTree, const char *sourceText,
 										const Oid *paramTypes, int numParams, QueryEnvironment *queryEnv);
 extern Query *parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
-									  Oid **paramTypes, int *numParams, QueryEnvironment *queryEnv);
+									  Oid **paramTypes, int *numParams,
+									  Oid **paramOrigTbls, AttrNumber **paramOrigCols,
+									  QueryEnvironment *queryEnv);
 extern Query *parse_analyze_withcb(RawStmt *parseTree, const char *sourceText,
 								   ParserSetupHook parserSetup,
 								   void *parserSetupArg,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index ae35f03251..13ccadfd5d 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -152,6 +152,7 @@ PG_KEYWORD("empty", EMPTY_P, UNRESERVED_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("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -268,6 +269,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 cf9c759025..225a2a8733 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -93,6 +93,7 @@ typedef Node *(*ParseParamRefHook) (ParseState *pstate, ParamRef *pref);
 typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param,
 								  Oid targetTypeId, int32 targetTypeMod,
 								  int location);
+typedef void (*ParamAssignOrigHook) (ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 
 
 /*
@@ -222,6 +223,7 @@ struct ParseState
 	PostParseColumnRefHook p_post_columnref_hook;
 	ParseParamRefHook p_paramref_hook;
 	CoerceParamHook p_coerce_param_hook;
+	ParamAssignOrigHook p_param_assign_orig_hook;
 	void	   *p_ref_hook_state;	/* common passthrough link for above */
 };
 
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index df1ee660d8..da202b28a7 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -18,7 +18,8 @@
 extern void setup_parse_fixed_parameters(ParseState *pstate,
 										 const Oid *paramTypes, int numParams);
 extern void setup_parse_variable_parameters(ParseState *pstate,
-											Oid **paramTypes, int *numParams);
+											Oid **paramTypes, int *numParams,
+											Oid **paramOrigTbls, AttrNumber **paramOrigCols);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 2b1163ce33..70c2cfd05b 100644
--- a/src/include/tcop/cmdtaglist.h
+++ b/src/include/tcop/cmdtaglist.h
@@ -86,6 +86,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)
diff --git a/src/include/tcop/tcopprot.h b/src/include/tcop/tcopprot.h
index 70d9dab25b..3038ceb522 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -53,6 +53,8 @@ extern List *pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 											  const char *query_string,
 											  Oid **paramTypes,
 											  int *numParams,
+											  Oid **paramOrigTbls,
+											  AttrNumber **paramOrigCols,
 											  QueryEnvironment *queryEnv);
 extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree,
 										   const char *query_string,
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index 0499635f59..9a7cf794ce 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 4463ea66be..d0b81766c7 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,13 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATAOID,
+	CEKNAME,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAME,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index b5fd72a4ac..5a065eb80b 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..fabad38ac0 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,7 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPrepared2           187
+PQsendQueryPrepared2      188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index dc49387d6c..f3c15ece4d 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -344,6 +344,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
 	offsetof(struct pg_conn, target_session_attrs)},
 
+	{"cmklookup", "PGCMKLOOKUP", "", NULL,
+		"CMK-Lookup", "", 64,
+	offsetof(struct pg_conn, cmklookup)},
+
 	/* Terminating entry --- MUST BE LAST */
 	{NULL, NULL, NULL, NULL,
 	NULL, NULL, 0}
@@ -4097,6 +4101,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 0000000000..524ba8a0f9
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,758 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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;
+
+	evp_md_ctx = EVP_MD_CTX_new();
+
+	pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_HMAC, NULL, mac_key, mac_key_len);
+	if (!pkey)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("could not allocate key for HMAC: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out out memory");
+		goto fail;
+	}
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(cekalg);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	*(int64 *) (buf + PG_AD_LEN + encrlen) = pg_hton64(PG_AD_LEN * 8);
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, buf, bufsize))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, md_len_p))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("digest signing failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	Assert(*md_len_p == md_hash_length(md));
+
+	/* truncate output to half the length, per spec */
+	*md_len_p /= 2;
+
+	result = true;
+fail:
+	free(buf);
+	EVP_PKEY_free(pkey);
+	EVP_MD_CTX_free(evp_md_ctx);
+	return result;
+}
+
+unsigned char *
+decrypt_value(PGresult *res, const PGCEK *cek, int cekalg, const unsigned char *input, int inputlen, const char **errmsgp)
+{
+	static char msgbuf[1024];
+
+	const unsigned char *iv = NULL;
+	size_t		ivlen;
+
+	const EVP_CIPHER *cipher;
+	const EVP_MD *md;
+	EVP_CIPHER_CTX *evp_cipher_ctx = NULL;
+	int			enc_key_len;
+	int			mac_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	unsigned char md_value[EVP_MAX_MD_SIZE];
+	size_t		md_len = sizeof(md_value);
+	size_t		bufsize;
+	unsigned char *buf = NULL;
+	unsigned char *decr;
+	int			decrlen,
+				decrlen2;
+
+	unsigned char *result = NULL;
+
+	cipher = pg_cekalg_to_openssl_cipher(cekalg);
+	if (!cipher)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized encryption algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("unrecognized digest algorithm identifier: %d"), cekalg);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("column encryption key has wrong length for algorithm (has: %zu, required: %d)"),
+				 cek->cekdatalen, key_len);
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	enc_key = cek->cekdata + mac_key_len;
+	mac_key = cek->cekdata;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  cekalg,
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	if (memcmp(input + (inputlen - md_len), md_value, md_len) != 0)
+	{
+		*errmsgp = libpq_gettext("MAC mismatch");
+		goto fail;
+	}
+
+	ivlen = EVP_CIPHER_CTX_iv_length(evp_cipher_ctx);
+	iv = input;
+	input += ivlen;
+	inputlen -= ivlen;
+	if (!EVP_DecryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption initialization failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+
+	bufsize = inputlen + EVP_CIPHER_CTX_block_size(evp_cipher_ctx) + 1;
+#ifndef TEST_ENCRYPT
+	buf = pqResultAlloc(res, bufsize, false);
+#else
+	buf = malloc(bufsize);
+#endif
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out out memory");
+		goto fail;
+	}
+	decr = buf;
+	if (!EVP_DecryptUpdate(evp_cipher_ctx, decr, &decrlen, input, inputlen - md_len))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	if (!EVP_DecryptFinal_ex(evp_cipher_ctx, decr + decrlen, &decrlen2))
+	{
+		snprintf(msgbuf, sizeof(msgbuf),
+				 libpq_gettext("decryption failed: %s"),
+				 ERR_reason_error_string(ERR_get_error()));
+		*errmsgp = msgbuf;
+		goto fail;
+	}
+	decrlen += decrlen2;
+	Assert(decrlen < bufsize);
+	decr[decrlen] = '\0';
+	result = decr;
+
+fail:
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	return result;
+}
+
+#ifndef TEST_ENCRYPT
+static bool
+make_siv(PGconn *conn,
+		 unsigned char *iv, size_t ivlen,
+		 const EVP_MD *md,
+		 const unsigned char *iv_key, int iv_key_len,
+		 const unsigned char *plaintext, int plaintext_len)
+{
+	EVP_MD_CTX *evp_md_ctx = NULL;
+	EVP_PKEY   *pkey = NULL;
+	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 0000000000..b0093dc527
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * encryption support
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FE_ENCRYPT_H
+#define FE_ENCRYPT_H
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+extern unsigned char *decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg,
+											int fromlen, const unsigned char *from,
+											int *tolen);
+
+extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg,
+									const unsigned char *input, int inputlen,
+									const char **errmsgp);
+
+extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg,
+									const unsigned char *value, int *nbytesp, bool enc_det);
+
+#endif							/* FE_ENCRYPT_H */
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index a36f5eb310..4cb2ca5a79 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,381 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+int
+pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+					  const char *keyrealm)
+{
+	char	   *keyname_copy;
+	char	   *keyrealm_copy;
+	bool		found;
+
+	keyname_copy = strdup(keyname);
+	if (!keyname_copy)
+		return EOF;
+	keyrealm_copy = strdup(keyrealm);
+	if (!keyrealm_copy)
+	{
+		free(keyname_copy);
+		return EOF;
+	}
+
+	found = false;
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		struct pg_cmk *checkcmk = &conn->cmks[i];
+
+		/* replace existing? */
+		if (checkcmk->cmkid == keyid)
+		{
+			free(checkcmk->cmkname);
+			free(checkcmk->cmkrealm);
+			checkcmk->cmkname = keyname_copy;
+			checkcmk->cmkrealm = keyrealm_copy;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newncmks;
+		struct pg_cmk *newcmks;
+		struct pg_cmk *newcmk;
+
+		newncmks = conn->ncmks + 1;
+		if (newncmks <= 0)
+			return EOF;
+		newcmks = realloc(conn->cmks, newncmks * sizeof(struct pg_cmk));
+		if (!newcmks)
+		{
+			free(keyname_copy);
+			free(keyrealm_copy);
+			return EOF;
+		}
+
+		newcmk = &newcmks[newncmks - 1];
+		newcmk->cmkid = keyid;
+		newcmk->cmkname = keyname_copy;
+		newcmk->cmkrealm = keyrealm_copy;
+
+		conn->ncmks = newncmks;
+		conn->cmks = newcmks;
+	}
+
+	return 0;
+}
+
+/*
+ * Replace placeholders in input string.  Return value malloc'ed.
+ */
+static char *
+replace_cmk_placeholders(const char *in, const char *cmkname, const char *cmkrealm, int cmkalg, const char *b64data)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s;
+
+						switch (cmkalg)
+						{
+							case PG_CMK_RSAES_OAEP_SHA_1:
+								s = "RSAES_OAEP_SHA_1";
+								break;
+							case PG_CMK_RSAES_OAEP_SHA_256:
+								s = "RSAES_OAEP_SHA_256";
+								break;
+							default:
+								s = "INVALID";
+						}
+						appendPQExpBufferStr(&buf, s);
+					}
+					p++;
+					break;
+				case 'b':
+					appendPQExpBufferStr(&buf, b64data);
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'r':
+					appendPQExpBufferStr(&buf, cmkrealm);
+					p++;
+					break;
+				default:
+					appendPQExpBufferChar(&buf, p[0]);
+			}
+		}
+		else
+			appendPQExpBufferChar(&buf, p[0]);
+	}
+
+	return buf.data;
+}
+
+#ifndef USE_SSL
+/*
+ * Dummy implementation for non-SSL builds
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	appendPQExpBuffer(&conn->errorMessage,
+					  libpq_gettext("column encryption not supported by this build\n"));
+	return NULL;
+}
+#endif
+
+/*
+ * Decrypt a CEK using the given CMK.  The ciphertext is passed in
+ * "from" and "fromlen".  Return the decrypted value in a malloc'ed area, its
+ * length via "tolen".  Return NULL on error; add error messages directly to
+ * "conn".
+ */
+static unsigned char *
+decrypt_cek(PGconn *conn, const PGCMK *cmk, int cmkalg,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	char	   *cmklookup;
+	bool		found = false;
+	unsigned char *result = NULL;
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+		return NULL;
+	}
+
+	/*
+	 * Analyze semicolon-separated list
+	 */
+	for (char *s = strtok(cmklookup, ";"); s; s = strtok(NULL, ";"))
+	{
+		char	   *sep;
+
+		/* split found token at '=' */
+		sep = strchr(s, '=');
+		if (!sep)
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("syntax error in CMK lookup specification, missing \"%c\": %s\n"), '=', s);
+			break;
+		}
+
+		/* matching realm? */
+		if (strncmp(s, "*", sep - s) == 0 || strncmp(s, cmk->cmkrealm, sep - s) == 0)
+		{
+			char	   *sep2;
+
+			found = true;
+
+			sep2 = strchr(sep, ':');
+			if (!sep2)
+			{
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("syntax error in CMK lookup specification, missing \"%c\": %s\n"), ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+				FILE	   *fp;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				fp = fopen(cmkfilename, "rb");
+				if (fp)
+				{
+					result = decrypt_cek_from_file(conn, fp, cmkalg, fromlen, from, tolen);
+					fclose(fp);
+				}
+				else
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not open file \"%s\": %m\n"), cmkfilename);
+					free(cmkfilename);
+					goto fail;
+				}
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				int			enclen;
+				char	   *enc;
+				char	   *command;
+				FILE	   *fp;
+				char		buf[4096];
+
+				enclen = pg_b64_enc_len(fromlen);
+				enc = malloc(enclen + 1);
+				if (!enc)
+				{
+					appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+					goto fail;
+				}
+				enclen = pg_b64_encode((const char *) from, fromlen, enc, enclen);
+				if (enclen < 0)
+				{
+					appendPQExpBuffer(&conn->errorMessage, libpq_gettext("base64 encoding failed\n"));
+					free(enc);
+					goto fail;
+				}
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, enc);
+				free(enc);
+				fp = popen(command, "r");
+				free(command);
+				if (!fp)
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not run command \"%s\": %m\n"), command);
+					goto fail;
+				}
+				if (fgets(buf, sizeof(buf), fp))
+				{
+					int			linelen;
+					int			declen;
+					char	   *dec;
+
+					linelen = strlen(buf);
+					if (buf[linelen - 1] == '\n')
+					{
+						buf[linelen - 1] = '\0';
+						linelen--;
+					}
+					declen = pg_b64_dec_len(linelen);
+					dec = malloc(declen);
+					if (!dec)
+					{
+						appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+						goto fail;
+					}
+					declen = pg_b64_decode(buf, linelen, dec, declen);
+					if (declen < 0)
+					{
+						appendPQExpBuffer(&conn->errorMessage,
+										  libpq_gettext("base64 decoding failed\n"));
+						free(dec);
+						goto fail;
+					}
+					result = (unsigned char *) dec;
+					*tolen = declen;
+				}
+				else
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not read from command: %m\n"));
+					pclose(fp);
+					goto fail;
+				}
+				pclose(fp);
+			}
+			else
+			{
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("CMK lookup scheme \"%s\" not recognized\n"), sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("no CMK lookup found for realm \"%s\"\n"), cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+int
+pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, const unsigned char *value, int len)
+{
+	PGCMK	   *cmk = NULL;
+	unsigned char *plainval = NULL;
+	int			plainvallen = 0;
+	bool		found;
+
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		if (conn->cmks[i].cmkid == cmkid)
+		{
+			cmk = &conn->cmks[i];
+			break;
+		}
+	}
+
+	if (!cmk)
+		return EOF;
+
+	plainval = decrypt_cek(conn, cmk, cmkalg, len, value, &plainvallen);
+	if (!plainval)
+		return EOF;
+
+	found = false;
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		struct pg_cek *checkcek = &conn->ceks[i];
+
+		/* replace existing? */
+		if (checkcek->cekid == keyid)
+		{
+			free(checkcek->cekdata);
+			checkcek->cekdata = plainval;
+			checkcek->cekdatalen = plainvallen;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newnceks;
+		struct pg_cek *newceks;
+		struct pg_cek *newcek;
+
+		newnceks = conn->nceks + 1;
+		if (newnceks <= 0)
+		{
+			free(plainval);
+			return EOF;
+		}
+		newceks = realloc(conn->ceks, newnceks * sizeof(struct pg_cek));
+		if (!newceks)
+		{
+			free(plainval);
+			return EOF;
+		}
+
+		newcek = &newceks[newnceks - 1];
+		newcek->cekid = keyid;
+		newcek->cekdata = plainval;
+		newcek->cekdatalen = plainvallen;
+
+		conn->nceks = newnceks;
+		conn->ceks = newceks;
+	}
+
+	return 0;
+}
 
 /*
  * pqRowProcessor
@@ -1250,9 +1630,58 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 		}
 		else
 		{
-			bool		isbinary = (res->attDescs[i].format != 0);
+			bool		isbinary = ((res->attDescs[i].format & 0x0F) != 0);
 			char	   *val;
 
+			if (res->attDescs[i].cekid)
+			{
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				for (int j = 0; j < conn->nceks; j++)
+				{
+					if (conn->ceks[j].cekid == res->attDescs[i].cekid)
+					{
+						cek = &conn->ceks[j];
+						break;
+					}
+				}
+				if (!cek)
+				{
+					*errmsgp = libpq_gettext("column encryption key not found");
+					goto fail;
+				}
+
+				if (isbinary)
+				{
+					val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+												 (const unsigned char *) columns[i].value, clen, errmsgp);
+				}
+				else
+				{
+					unsigned char *buf;
+					unsigned char *enccolval;
+					size_t		enccolvallen;
+
+					buf = malloc(clen + 1);
+					memcpy(buf, columns[i].value, clen);
+					buf[clen] = '\0';
+					enccolval = PQunescapeBytea(buf, &enccolvallen);
+					val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+												 enccolval, enccolvallen, errmsgp);
+					free(enccolval);
+					free(buf);
+				}
+
+				if (val == NULL)
+					goto fail;
+#else
+				*errmsgp = libpq_gettext("column encryption not supported by this build");
+				goto fail;
+#endif
+			}
+			else
+			{
 			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
 			if (val == NULL)
 				goto fail;
@@ -1260,6 +1689,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;
@@ -1587,7 +2017,9 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   NULL,
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1705,6 +2137,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;
@@ -1732,7 +2178,9 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   paramForceColumnEncryptions,
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1833,7 +2281,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;
@@ -1880,14 +2330,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;
 		}
 	}
@@ -1906,6 +2395,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1924,9 +2414,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
 		{
@@ -2380,12 +2915,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);
 }
@@ -3683,6 +4233,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)
 {
@@ -3868,6 +4429,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 f267dfd33c..19ec572764 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -45,6 +45,8 @@ static int	getRowDescriptions(PGconn *conn, int msgLength);
 static int	getParamDescriptions(PGconn *conn, int msgLength);
 static int	getAnotherTuple(PGconn *conn, int msgLength);
 static int	getParameterStatus(PGconn *conn);
+static int	getColumnMasterKey(PGconn *conn);
+static int	getColumnEncryptionKey(PGconn *conn);
 static int	getNotify(PGconn *conn);
 static int	getCopyStart(PGconn *conn, ExecStatusType copytype);
 static int	getReadyForQuery(PGconn *conn);
@@ -319,6 +321,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 &&
@@ -576,6 +594,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) ||
@@ -583,7 +603,9 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			pqGetInt(&typid, 4, conn) ||
 			pqGetInt(&typlen, 2, conn) ||
 			pqGetInt(&atttypmod, 4, conn) ||
-			pqGetInt(&format, 2, conn))
+			pqGetInt(&format, 2, conn) ||
+			pqGetInt(&cekid, 4, conn) ||
+			pqGetInt(&cekalg, 2, conn))
 		{
 			/* We should not run out of data here, so complain */
 			errmsg = libpq_gettext("insufficient data in \"T\" message");
@@ -611,8 +633,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;
 	}
 
@@ -714,10 +738,22 @@ getParamDescriptions(PGconn *conn, int msgLength)
 	for (i = 0; i < nparams; i++)
 	{
 		int			typid;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGetInt(&typid, 4, conn))
 			goto not_enough_data;
 		result->paramDescs[i].typid = typid;
+		if (pqGetInt(&cekid, 4, conn))
+			goto not_enough_data;
+		result->paramDescs[i].cekid = cekid;
+		if (pqGetInt(&cekalg, 2, conn))
+			goto not_enough_data;
+		result->paramDescs[i].cekalg = cekalg;
+		if (pqGetInt(&flags, 2, conn))
+			goto not_enough_data;
+		result->paramDescs[i].flags = flags;
 	}
 
 	/* Success! */
@@ -1443,6 +1479,89 @@ getParameterStatus(PGconn *conn)
 	return 0;
 }
 
+/*
+ * Attempt to read a ColumnMasterKey message.
+ * Entry: 'y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnMasterKey(PGconn *conn)
+{
+	int			keyid;
+	char	   *keyname;
+	char	   *keyrealm;
+	int			ret;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the key name */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyname = strdup(conn->workBuffer.data);
+	if (!keyname)
+		return EOF;
+	/* Get the key realm */
+	if (pqGets(&conn->workBuffer, conn) != 0)
+		return EOF;
+	keyrealm = strdup(conn->workBuffer.data);
+	if (!keyrealm)
+		return EOF;
+	/* And save it */
+	ret = pqSaveColumnMasterKey(conn, keyid, keyname, keyrealm);
+
+	free(keyname);
+	free(keyrealm);
+
+	return ret;
+}
+
+/*
+ * Attempt to read a ColumnEncryptionKey message.
+ * Entry: 'Y' message type and length have already been consumed.
+ * Exit: returns 0 if successfully consumed message.
+ *		 returns EOF if not enough data.
+ */
+static int
+getColumnEncryptionKey(PGconn *conn)
+{
+	int			keyid;
+	int			cmkid;
+	int			cmkalg;
+	char	   *buf;
+	int			vallen;
+
+	/* Get the key ID */
+	if (pqGetInt(&keyid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK ID */
+	if (pqGetInt(&cmkid, 4, conn) != 0)
+		return EOF;
+	/* Get the CMK algorithm */
+	if (pqGetInt(&cmkalg, 2, conn) != 0)
+		return EOF;
+	/* Get the key data len */
+	if (pqGetInt(&vallen, 4, conn) != 0)
+		return EOF;
+	/* Get the key data */
+	buf = malloc(vallen);
+	if (!buf)
+		return EOF;
+	if (pqGetnchar(buf, vallen, conn) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	/* And save it */
+	if (pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen) != 0)
+	{
+		free(buf);
+		return EOF;
+	}
+	free(buf);
+	return 0;
+}
 
 /*
  * Attempt to read a Notify response message.
diff --git a/src/interfaces/libpq/fe-trace.c b/src/interfaces/libpq/fe-trace.c
index 5d68cf2eb3..e08cbcff33 100644
--- a/src/interfaces/libpq/fe-trace.c
+++ b/src/interfaces/libpq/fe-trace.c
@@ -458,7 +458,12 @@ pqTraceOutputt(FILE *f, const char *message, int *cursor, bool regress)
 	nfields = pqTraceOutputInt16(f, message, cursor);
 
 	for (int i = 0; i < nfields; i++)
+	{
+		pqTraceOutputInt32(f, message, cursor, regress);
 		pqTraceOutputInt32(f, message, cursor, regress);
+		pqTraceOutputInt16(f, message, cursor);
+		pqTraceOutputInt16(f, message, cursor);
+	}
 }
 
 /* RowDescription */
@@ -479,6 +484,8 @@ pqTraceOutputT(FILE *f, const char *message, int *cursor, bool regress)
 		pqTraceOutputInt16(f, message, cursor);
 		pqTraceOutputInt32(f, message, cursor, false);
 		pqTraceOutputInt16(f, message, cursor);
+		pqTraceOutputInt32(f, message, cursor, regress);
+		pqTraceOutputInt16(f, message, cursor);
 	}
 }
 
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 7986445f1a..42fbe0cd49 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -267,6 +267,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -438,6 +440,15 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPrepared2(PGconn *conn,
+								 const char *stmtName,
+								 int nParams,
+								 const char *const *paramValues,
+								 const int *paramLengths,
+								 const int *paramFormats,
+								 const int *paramForceColumnEncryptions,
+								 int resultFormat,
+								 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +472,15 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPrepared2(PGconn *conn,
+								 const char *stmtName,
+								 int nParams,
+								 const char *const *paramValues,
+								 const int *paramLengths,
+								 const int *paramFormats,
+								 const int *paramForceColumnEncryptions,
+								 int resultFormat,
+								 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +551,7 @@ extern int	PQfformat(const PGresult *res, int field_num);
 extern Oid	PQftype(const PGresult *res, int field_num);
 extern int	PQfsize(const PGresult *res, int field_num);
 extern int	PQfmod(const PGresult *res, int field_num);
+extern int	PQfisencrypted(const PGresult *res, int field_num);
 extern char *PQcmdStatus(PGresult *res);
 extern char *PQoidStatus(const PGresult *res);	/* old and ugly */
 extern Oid	PQoidValue(const PGresult *res);	/* new and improved */
@@ -540,6 +561,7 @@ extern int	PQgetlength(const PGresult *res, int tup_num, int field_num);
 extern int	PQgetisnull(const PGresult *res, int tup_num, int field_num);
 extern int	PQnparams(const PGresult *res);
 extern Oid	PQparamtype(const PGresult *res, int param_num);
+extern int	PQparamisencrypted(const PGresult *res, int param_num);
 
 /* Describe prepared statements and portals */
 extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 51ab51f9f9..ed803b1f88 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -111,6 +111,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -342,6 +345,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.
@@ -395,6 +418,7 @@ struct pg_conn
 	char	   *ssl_min_protocol_version;	/* minimum TLS protocol version */
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
+	char	   *cmklookup;		/* CMK lookup specification */
 
 	/* Optional file to write trace info to */
 	FILE	   *Pfdebug;
@@ -477,6 +501,12 @@ struct pg_conn
 	PGContextVisibility show_context;	/* whether to show CONTEXT field */
 	PGlobjfuncs *lobjfuncs;		/* private state for large-object access fns */
 
+	/* Column encryption support data */
+	int			ncmks;
+	PGCMK	   *cmks;
+	int			nceks;
+	PGCEK	   *ceks;
+
 	/* Buffer for data received from backend and not yet processed */
 	char	   *inBuffer;		/* currently allocated buffer */
 	int			inBufSize;		/* allocated size of buffer */
@@ -672,6 +702,10 @@ extern void pqSaveMessageField(PGresult *res, char code,
 							   const char *value);
 extern void pqSaveParameterStatus(PGconn *conn, const char *name,
 								  const char *value);
+extern int	pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+								  const char *keyrealm);
+extern int	pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg,
+									  const unsigned char *value, int len);
 extern int	pqRowProcessor(PGconn *conn, const char **errmsgp);
 extern void pqCommandQueueAdvance(PGconn *conn);
 extern int	PQsendQueryContinue(PGconn *conn, const char *query);
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 9256b426c1..c45fe86d1d 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 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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/test/Makefile b/src/test/Makefile
index 69ef074d75..2300be8332 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -32,6 +32,7 @@ SUBDIRS += ldap
 endif
 endif
 ifeq ($(with_ssl),openssl)
+SUBDIRS += column_encryption
 ifneq (,$(filter ssl,$(PG_TEST_EXTRA)))
 SUBDIRS += ssl
 endif
@@ -41,7 +42,7 @@ endif
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl column_encryption)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..7aca84b17a
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,29 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/column_encryption/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..a0d3192800
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,180 @@
+# 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{'PGCMKLOOKUP'} = '*=file:' . ${PostgreSQL::Test::Utils::tmp_check} . '/%k.pem';
+
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl1 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl2 (
+    a int,
+    b text ENCRYPTED WITH (encryption_type = deterministic, column_encryption_key = cek1)
+);
+});
+
+$node->safe_psql('postgres', qq{
+CREATE TABLE tbl3 (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1),
+    c text ENCRYPTED WITH (column_encryption_key = cek2, algorithm = 'AEAD_AES_192_CBC_HMAC_SHA_384')
+);
+});
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl1 (a, b) VALUES (1, $1) \gencr 'val1'
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \gencr 'val2'
+});
+
+# Expected ciphertext length is 2 blocks of AES output (2 * 16) plus
+# half SHA-256 output (16) in hex encoding: (2 * 16 + 16) * 2 = 96
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1) TO STDOUT}),
+	qr/1\t\\\\x[0-9a-f]{96}\n2\t\\\\x[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+my $result;
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1 \gdesc});
+is($result,
+	q(a|integer
+b|text),
+	'query result description has original type');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'decrypted query result');
+
+{
+	local $ENV{'PGCMKLOOKUP'} = '*=run:broken %k "%b"';
+	$result = $node->psql('postgres', q{SELECT a, b FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{'PGCMKLOOKUP'} = "*=run:perl ./test_run_decrypt.pl '${PostgreSQL::Test::Utils::tmp_check}' %k %a '%b'";
+
+	$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+	is($result,
+		q(1|val1
+2|val2),
+		'decrypted query result with cmklookup run');
+}
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES (1, $1, $2) \gencr 'valB1' 'valC1'
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+
+$node->command_fails_like(['test_client', 'test1'], qr/not encrypted/, 'test client fails because parameters not encrypted');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2),
+	'decrypted query result after test client insert');
+
+$node->command_ok(['test_client', 'test2'], 'test client test 2');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3),
+	'decrypted query result after test client insert 2');
+
+like($node->safe_psql('postgres', q{COPY (SELECT * FROM tbl1 WHERE a = 3) TO STDOUT}),
+	qr/3\t\\\\x[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+$node->command_ok(['test_client', 'test3'], 'test client test 3');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl1});
+is($result,
+	q(1|val1
+2|val2
+3|val3upd),
+	'decrypted query result after test client insert 3');
+
+$node->command_ok(['test_client', 'test4'], 'test client test 4');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result after test client insert 4');
+
+is($node->safe_psql('postgres', q{SELECT b, count(*) FROM tbl2 GROUP BY b ORDER BY 2}),
+	q(valB|1
+valA|2),
+	'group by deterministically encrypted column');
+
+$node->command_ok(['test_client', 'test5'], 'test client test 5');
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1
+2|valB2|valC2
+3|valB3|valC3),
+	'decrypted query result multiple keys after test client insert 5');
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..cf6114db46
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,118 @@
+# 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{'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
+# TODO: There should be some ALTER COLUMN ENCRYPTION KEY command to do this.
+$node->safe_psql('postgres', qq{
+INSERT INTO pg_colenckeydata (oid, ckdcekid, ckdcmkid, ckdcmkalg, ckdencval) VALUES (
+    pg_nextoid('pg_catalog.pg_colenckeydata', 'oid', 'pg_catalog.pg_colenckeydata_oid_index'),
+    (SELECT oid FROM pg_colenckey WHERE cekname = '${cekname}'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk2'),
+    1,
+    '\\x${cekenchex}'
+);
+});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with two CMKs');
+
+
+# delete CEK record for first CMK
+$node->safe_psql('postgres', qq{
+DELETE FROM pg_colenckeydata WHERE ckdcmkid = (SELECT oid FROM pg_colmasterkey WHERE cmkname = 'cmk1');
+});
+
+
+is($node->safe_psql('postgres', q{SELECT a, b FROM tbl1}),
+	q(1|val1
+2|val2),
+	'decrypted query result with only new CMK');
+
+
+done_testing();
diff --git a/src/test/column_encryption/test_client.c b/src/test/column_encryption/test_client.c
new file mode 100644
index 0000000000..add9fe40a6
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2021-2022, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3"};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					   2, NULL, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3"};
+	int			forces[] = {false, true};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl1 (a, b) VALUES ($1, $2)",
+					2, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (!(!PQparamisencrypted(res2, 0) &&
+		  PQparamisencrypted(res2, 1)))
+	{
+		fprintf(stderr, "wrong results from PQparamisencrypted()\n");
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, forces, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3upd"};
+
+	res = PQprepare(conn, "", "UPDATE tbl1 SET b = $2 WHERE a = $1",
+					2, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 2, values, NULL, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"1", "valA", "2", "valB", "3", "valA"};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6)",
+					6, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 6, values, NULL, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+static int
+test5(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {
+		"2", "valB2", "valC2",
+		"3", "valB3", "valC3"
+	};
+
+	res = PQprepare(conn, "", "INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6)",
+					6, NULL);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQprepare() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res2 = PQdescribePrepared(conn, "");
+	if (PQresultStatus(res2) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQdescribePrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	res = PQexecPrepared2(conn, "", 6, values, NULL, NULL, NULL, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+int
+main(int argc, char **argv)
+{
+	PGconn	   *conn;
+	int			ret = 0;
+
+	conn = PQconnectdb("");
+	if (PQstatus(conn) != CONNECTION_OK)
+	{
+		fprintf(stderr, "Connection to database failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	if (argc < 2 || argv[1] == NULL)
+		return 87;
+	else if (strcmp(argv[1], "test1") == 0)
+		ret = test1(conn);
+	else if (strcmp(argv[1], "test2") == 0)
+		ret = test2(conn);
+	else if (strcmp(argv[1], "test3") == 0)
+		ret = test3(conn);
+	else if (strcmp(argv[1], "test4") == 0)
+		ret = test4(conn);
+	else if (strcmp(argv[1], "test5") == 0)
+		ret = test5(conn);
+	else
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..9664349f14
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,41 @@
+#!/usr/bin/perl
+
+# Test/sample command for libpq cmklookup run scheme
+#
+# This just places the data into temporary files and runs the openssl
+# command on it.  (In practice, this could more simply be written as a
+# shell script, but this way it's more portable.)
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use MIME::Base64;
+
+my ($tmpdir, $cmkname, $alg, $b64data) = @ARGV;
+
+die unless $alg eq 'RSAES_OAEP_SHA_1';
+
+open my $fh, '>:raw', "${tmpdir}/input.tmp" or die;
+print $fh decode_base64($b64data);
+close $fh;
+
+system('openssl', 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', "${tmpdir}/input.tmp", '-out', "${tmpdir}/output.tmp") == 0 or die;
+
+open $fh, '<:raw', "${tmpdir}/output.tmp" or die;
+my $data = '';
+
+while (1) {
+	my $success = read $fh, $data, 100, length($data);
+	die $! if not defined $success;
+	last if not $success;
+}
+
+close $fh;
+
+unlink "${tmpdir}/input.tmp", "${tmpdir}/output.tmp";
+
+print encode_base64($data);
diff --git a/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace b/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace
index dd6df03f1e..5fc9c0346d 100644
--- a/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace
+++ b/src/test/modules/libpq_pipeline/traces/disallowed_in_pipeline.trace
@@ -1,5 +1,5 @@
 F	13	Query	 "SELECT 1"
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace b/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace
index 4b9ab07ca4..700b0b6519 100644
--- a/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace
+++ b/src/test/modules/libpq_pipeline/traces/multi_pipelines.trace
@@ -10,13 +10,13 @@ F	9	Execute	 "" 0
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/nosync.trace b/src/test/modules/libpq_pipeline/traces/nosync.trace
index d99aac649d..7b1dfa1c15 100644
--- a/src/test/modules/libpq_pipeline/traces/nosync.trace
+++ b/src/test/modules/libpq_pipeline/traces/nosync.trace
@@ -41,52 +41,52 @@ F	9	Execute	 "" 0
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 B	4	ParseComplete
 B	4	BindComplete
-B	31	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0
+B	37	RowDescription	 1 "repeat" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	70	DataRow	 1 60 'xyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxzxyzxz'
 B	13	CommandComplete	 "SELECT 1"
 F	4	Terminate
diff --git a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
index 3fce548b99..6e4309cadd 100644
--- a/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
+++ b/src/test/modules/libpq_pipeline/traces/pipeline_abort.trace
@@ -50,14 +50,14 @@ F	6	Close	 P ""
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 65535 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 65535 -1 0 NNNN 0
 B	32	DataRow	 1 22 '0.33333333333333333333'
 B	32	DataRow	 1 22 '0.50000000000000000000'
 B	32	DataRow	 1 22 '1.00000000000000000000'
 B	NN	ErrorResponse	 S "ERROR" V "ERROR" C "22012" M "division by zero" F "SSSS" L "SSSS" R "SSSS" \x00
 B	5	ReadyForQuery	 I
 F	40	Query	 "SELECT itemno FROM pq_pipeline_demo"
-B	31	RowDescription	 1 "itemno" NNNN 2 NNNN 4 -1 0
+B	37	RowDescription	 1 "itemno" NNNN 2 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '3'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/pipeline_idle.trace b/src/test/modules/libpq_pipeline/traces/pipeline_idle.trace
index 3957ee4dfe..d937d1b8f3 100644
--- a/src/test/modules/libpq_pipeline/traces/pipeline_idle.trace
+++ b/src/test/modules/libpq_pipeline/traces/pipeline_idle.trace
@@ -6,7 +6,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
@@ -20,12 +20,12 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
 F	13	Query	 "SELECT 2"
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '2'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
@@ -37,7 +37,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
@@ -49,7 +49,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '2'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
@@ -61,7 +61,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
@@ -73,7 +73,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '2'
 B	13	CommandComplete	 "SELECT 1"
 B	4	CloseComplete
@@ -85,7 +85,7 @@ F	6	Close	 P ""
 F	4	Flush
 B	4	ParseComplete
 B	4	BindComplete
-B	43	RowDescription	 1 "pg_advisory_unlock" NNNN 0 NNNN 1 -1 0
+B	49	RowDescription	 1 "pg_advisory_unlock" NNNN 0 NNNN 1 -1 0 NNNN 0
 B	NN	NoticeResponse	 S "WARNING" V "WARNING" C "01000" M "you don't own a lock of type ExclusiveLock" F "SSSS" L "SSSS" R "SSSS" \x00
 B	11	DataRow	 1 1 'f'
 B	13	CommandComplete	 "SELECT 1"
diff --git a/src/test/modules/libpq_pipeline/traces/prepared.trace b/src/test/modules/libpq_pipeline/traces/prepared.trace
index 1a7de5c3e6..8d45bbc0ce 100644
--- a/src/test/modules/libpq_pipeline/traces/prepared.trace
+++ b/src/test/modules/libpq_pipeline/traces/prepared.trace
@@ -2,8 +2,8 @@ F	68	Parse	 "select_one" "SELECT $1, '42', $1::numeric, interval '1 sec'" 1 NNNN
 F	16	Describe	 S "select_one"
 F	4	Sync
 B	4	ParseComplete
-B	10	ParameterDescription	 1 NNNN
-B	113	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 "?column?" NNNN 0 NNNN 65535 -1 0 "numeric" NNNN 0 NNNN 65535 -1 0 "interval" NNNN 0 NNNN 16 -1 0
+B	18	ParameterDescription	 1 NNNN NNNN 0 0
+B	137	RowDescription	 4 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0 "?column?" NNNN 0 NNNN 65535 -1 0 NNNN 0 "numeric" NNNN 0 NNNN 65535 -1 0 NNNN 0 "interval" NNNN 0 NNNN 16 -1 0 NNNN 0
 B	5	ReadyForQuery	 I
 F	10	Query	 "BEGIN"
 B	10	CommandComplete	 "BEGIN"
@@ -13,6 +13,6 @@ B	19	CommandComplete	 "DECLARE CURSOR"
 B	5	ReadyForQuery	 T
 F	16	Describe	 P "cursor_one"
 F	4	Sync
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	5	ReadyForQuery	 T
 F	4	Terminate
diff --git a/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace b/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace
index 5c94749bc1..5f4849425d 100644
--- a/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace
+++ b/src/test/modules/libpq_pipeline/traces/simple_pipeline.trace
@@ -5,7 +5,7 @@ F	9	Execute	 "" 0
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	33	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0
+B	39	RowDescription	 1 "?column?" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '1'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/modules/libpq_pipeline/traces/singlerow.trace b/src/test/modules/libpq_pipeline/traces/singlerow.trace
index 9de99befcc..199df4d4f4 100644
--- a/src/test/modules/libpq_pipeline/traces/singlerow.trace
+++ b/src/test/modules/libpq_pipeline/traces/singlerow.trace
@@ -13,14 +13,14 @@ F	9	Execute	 "" 0
 F	4	Sync
 B	4	ParseComplete
 B	4	BindComplete
-B	40	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0
+B	46	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	12	DataRow	 1 2 '42'
 B	12	DataRow	 1 2 '43'
 B	12	DataRow	 1 2 '44'
 B	13	CommandComplete	 "SELECT 3"
 B	4	ParseComplete
 B	4	BindComplete
-B	40	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0
+B	46	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	12	DataRow	 1 2 '42'
 B	12	DataRow	 1 2 '43'
 B	12	DataRow	 1 2 '44'
@@ -28,7 +28,7 @@ B	12	DataRow	 1 2 '45'
 B	13	CommandComplete	 "SELECT 4"
 B	4	ParseComplete
 B	4	BindComplete
-B	40	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0
+B	46	RowDescription	 1 "generate_series" NNNN 0 NNNN 4 -1 0 NNNN 0
 B	12	DataRow	 1 2 '42'
 B	12	DataRow	 1 2 '43'
 B	12	DataRow	 1 2 '44'
diff --git a/src/test/modules/libpq_pipeline/traces/transaction.trace b/src/test/modules/libpq_pipeline/traces/transaction.trace
index 1dcc2373c0..a6869c4a5b 100644
--- a/src/test/modules/libpq_pipeline/traces/transaction.trace
+++ b/src/test/modules/libpq_pipeline/traces/transaction.trace
@@ -54,7 +54,7 @@ B	15	CommandComplete	 "INSERT 0 1"
 B	5	ReadyForQuery	 I
 B	5	ReadyForQuery	 I
 F	34	Query	 "SELECT * FROM pq_pipeline_tst"
-B	27	RowDescription	 1 "id" NNNN 1 NNNN 4 -1 0
+B	33	RowDescription	 1 "id" NNNN 1 NNNN 4 -1 0 NNNN 0
 B	11	DataRow	 1 1 '3'
 B	13	CommandComplete	 "SELECT 1"
 B	5	ReadyForQuery	 I
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..7f78dde6e1
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,37 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+CREATE COLUMN ENCRYPTION KEY cek1 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    |           |          |         | extended | cek1       |              | 
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..2aa0e16323 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attrealtypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,7 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 86d755aa44..4a366074da 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -175,13 +175,14 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+(7 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +198,13 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
  anyrange                    | anymultirange
-(5 rows)
+(6 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -841,6 +843,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+pg_encrypted_det_eq(pg_encrypted_det,pg_encrypted_det)
+pg_encrypted_det_ne(pg_encrypted_det,pg_encrypted_det)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
@@ -988,7 +992,9 @@ WHERE c.castmethod = 'b' AND
  xml               | text              |        0 | a
  xml               | character varying |        0 | a
  xml               | character         |        0 | a
-(10 rows)
+ bytea             | pg_encrypted_det  |        0 | e
+ bytea             | pg_encrypted_rnd  |        0 | e
+(12 rows)
 
 -- **************** pg_conversion ****************
 -- Look for illegal values in pg_conversion fields.
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39..4482a65d24 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 60acbd1241..33956f6993 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -237,6 +237,31 @@ SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g
  3 | Hello    | 4 | t
 (1 row)
 
+-- \gencr
+-- (This just tests the parameter passing; there is no encryption here.)
+CREATE TABLE test_gencr (a int, b text);
+INSERT INTO test_gencr VALUES (1, 'one') \gencr
+SELECT * FROM test_gencr WHERE a = 1 \gencr
+ a |  b  
+---+-----
+ 1 | one
+(1 row)
+
+INSERT INTO test_gencr VALUES ($1, $2) \gencr 2 'two'
+SELECT * FROM test_gencr WHERE a IN ($1, $2) \gencr 2 3
+ a |  b  
+---+-----
+ 2 | two
+(1 row)
+
+-- test parse error
+SELECT * FROM test_gencr WHERE a = x \gencr
+ERROR:  column "x" does not exist
+LINE 1: SELECT * FROM test_gencr WHERE a = x 
+                                           ^
+-- test bind error
+SELECT * FROM test_gencr WHERE a = $1 \gencr
+ERROR:  bind message supplies 0 parameters, but prepared statement "" requires 1
 -- \gexec
 create temporary table gexec_test(a int, b text, c date, d float);
 select format('create index on gexec_test(%I)', attname)
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 7ec3d2688f..42e38d66d1 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 d3ac08c9ee..fc0c5896e4 100644
--- a/src/test/regress/expected/type_sanity.out
+++ b/src/test/regress/expected/type_sanity.out
@@ -17,7 +17,7 @@ SELECT t1.oid, t1.typname
 FROM pg_type as t1
 WHERE t1.typnamespace = 0 OR
     (t1.typlen <= 0 AND t1.typlen != -1 AND t1.typlen != -2) OR
-    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r')) OR
+    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT t1.typisdefined OR
     (t1.typalign not in ('c', 's', 'i', 'd')) OR
     (t1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -75,7 +75,9 @@ ORDER BY t1.oid;
  4600 | pg_brin_bloom_summary
  4601 | pg_brin_minmax_multi_summary
  5017 | pg_mcv_list
-(6 rows)
+ 8243 | pg_encrypted_det
+ 8244 | pg_encrypted_rnd
+(8 rows)
 
 -- Make sure typarray points to a "true" array type of our own base
 SELECT t1.oid, t1.typname as basetype, t2.typname as arraytype,
@@ -210,7 +212,8 @@ ORDER BY 1;
  e       | enum_in
  m       | multirange_in
  r       | range_in
-(5 rows)
+ y       | byteain
+(6 rows)
 
 -- Check for bogus typoutput routines
 -- As of 8.0, this check finds refcursor, which is borrowing
@@ -255,7 +258,8 @@ ORDER BY 1;
  e       | enum_out
  m       | multirange_out
  r       | range_out
-(4 rows)
+ y       | byteaout
+(5 rows)
 
 -- Domains should have same typoutput as their base types
 SELECT t1.oid, t1.typname, t2.oid, t2.typname
@@ -335,7 +339,8 @@ ORDER BY 1;
  e       | enum_recv
  m       | multirange_recv
  r       | range_recv
-(5 rows)
+ y       | bytearecv
+(6 rows)
 
 -- Check for bogus typsend routines
 -- As of 7.4, this check finds refcursor, which is borrowing
@@ -380,7 +385,8 @@ ORDER BY 1;
  e       | enum_send
  m       | multirange_send
  r       | range_send
-(4 rows)
+ y       | byteasend
+(5 rows)
 
 -- Domains should have same typsend as their base types
 SELECT t1.oid, t1.typname, t2.oid, t2.typname
@@ -707,6 +713,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::pg_encrypted_rnd,
+  E'\\xDEADBEEF'::pg_encrypted_det,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 103e11483d..ffca206c6f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -129,6 +129,9 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # ----------
 test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats
 
+# WIP
+test: column_encryption
+
 # event_trigger cannot run concurrently with any test that runs DDL
 # oidjoins is read-only, though, and should run late for best coverage
 test: event_trigger oidjoins
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index a4b354c9e6..8ad1f458d5 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..0ca1d56210
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,28 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN ENCRYPTION KEY cek1 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
+
+DROP TABLE tbl_29f3;  -- FIXME: needs pg_dump support for pg_upgrade tests
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95c..7db788735c 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 1149c6a839..e88c70d302 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -119,6 +119,23 @@ CREATE TABLE bububu(a int) \gdesc
 -- all on one line
 SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g
 
+
+-- \gencr
+-- (This just tests the parameter passing; there is no encryption here.)
+
+CREATE TABLE test_gencr (a int, b text);
+INSERT INTO test_gencr VALUES (1, 'one') \gencr
+SELECT * FROM test_gencr WHERE a = 1 \gencr
+
+INSERT INTO test_gencr VALUES ($1, $2) \gencr 2 'two'
+SELECT * FROM test_gencr WHERE a IN ($1, $2) \gencr 2 3
+
+-- test parse error
+SELECT * FROM test_gencr WHERE a = x \gencr
+-- test bind error
+SELECT * FROM test_gencr WHERE a = $1 \gencr
+
+
 -- \gexec
 
 create temporary table gexec_test(a int, b text, c date, d float);
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 5edc1f1f6e..34dd19456d 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -20,7 +20,7 @@
 FROM pg_type as t1
 WHERE t1.typnamespace = 0 OR
     (t1.typlen <= 0 AND t1.typlen != -1 AND t1.typlen != -2) OR
-    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r')) OR
+    (t1.typtype not in ('b', 'c', 'd', 'e', 'm', 'p', 'r', 'y')) OR
     NOT t1.typisdefined OR
     (t1.typalign not in ('c', 's', 'i', 'd')) OR
     (t1.typstorage not in ('p', 'x', 'e', 'm'));
@@ -529,6 +529,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  E'\\xDEADBEEF'::pg_encrypted_rnd,
+  E'\\xDEADBEEF'::pg_encrypted_det,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,

base-commit: 1679d57a550530ebef624738cc1b12647714fca6
-- 
2.37.0

#23Masahiko Sawada
sawada.mshk@gmail.com
In reply to: Peter Eisentraut (#22)
Re: Transparent column encryption

On Tue, Jul 19, 2022 at 10:52 PM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

On 12.07.22 20:29, Peter Eisentraut wrote:

Updated patch, to resolve some merge conflicts.

Rebased patch, no new functionality

Thank you for working on this and updating the patch!

I've mainly looked at the documentation and tests and done some tests.
Before looking at the code in depth, I'd like to share my
comments/questions.

---
Regarding the documentation, I'd like to have a page that describes
the generic information of the transparent column encryption for users
such as what this feature actually does, what can be achieved by this
feature, CMK rotation, and its known limitations. The patch has
"Transparent Column Encryption" section in protocol.sgml but it seems
to be more internal information.

---
In datatype.sgml, it says "Thus, clients that don't support
transparent column encryption or have disabled it will see the
encrypted values as byte arrays." but I got an error rather than
encrypted values when I tried to connect to the server using by
clients that don't support the encryption:

postgres(1:6040)=# select * from tbl;
no CMK lookup found for realm ""

no CMK lookup found for realm ""
postgres(1:6040)=#

---
In single-user mode, the user cannot decrypt the encrypted value but
probably it's fine in practice.

---
Regarding the column master key rotation, would it be useful if we
provide a tool for that? For example, it takes old and new CMK as
input, re-encrypt all CEKs realted to the CMK, and registers them to
the server.

---
Is there any convenient way to load a large amount of test data to the
encrypted columns? I tried to use generate_series() but it seems not
to work as it generates the data on the server side:

postgres(1:80556)=# create table a (i text encrypted with
(column_encryption_key = cek1));
CREATE TABLE
postgres(1:80556)=# insert into a select i::text from
generate_series(1, 1000) i;
2022-07-20 15:06:38.502 JST [80556] ERROR: column "i" is of type
pg_encrypted_rnd but expression is of type text at character 22

I've also tried to load the data from a file on the client by using
\copy command, but it seems not to work:

postgres(1:80556)=# copy (select generate_series(1, 1000)::text) to
'/tmp/tmp.dat';
COPY 1000
postgres(1:80556)=# \copy a from '/tmp/tmp.dat'
COPY 1000
postgres(1:80556)=# select * from a;
out out memory

---
I got SEGV in the following two situations:

(1) SEGV by backend
postgres(1:59931)=# create table tbl (i int encrypted with
(column_encryption_key = cek1));
CREATE TABLE
postgres(1:59931)=# insert into tbl values ($1) \gencr 1
INSERT 0 1
postgres(1:59931)=# select * from tbl;
server closed the connection unexpectedly
This probably means the server terminated abnormally
before or while processing the request.

The backtrace is:

(lldb) bt
* thread #1, stop reason = signal SIGSTOP
* frame #0: 0x0000000106830a30
postgres`pg_detoast_datum_packed(datum=0xffffffffab32c563) at
fmgr.c:1742:6
frame #1: 0x00000001067d9dbf
postgres`byteaout(fcinfo=0x00007ffee9c311a8) at varlena.c:392:20
frame #2: 0x000000010682ed0c
postgres`FunctionCall1Coll(flinfo=0x00007faeb28193a8, collation=0,
arg1=18446744072286815587) at fmgr.c:1124:11
frame #3: 0x0000000106830611
postgres`OutputFunctionCall(flinfo=0x00007faeb28193a8,
val=18446744072286815587) at fmgr.c:1561:9
frame #4: 0x0000000105fed702
postgres`printtup(slot=0x00007faeb2818960, self=0x00007faeb3809390) at
printtup.c:519:16
frame #5: 0x0000000106319318
postgres`ExecutePlan(estate=0x00007faeb2818520,
planstate=0x00007faeb2818758, use_parallel_mode=false,
operation=CMD_SELECT, sendTuples=true, numberTuples=0,
direction=ForwardScanDirection, dest=0x00007faeb3809390,
execute_once=true) at execMain.c:1667:9
frame #6: 0x0000000106319180
postgres`standard_ExecutorRun(queryDesc=0x00007faeb280d920,
direction=ForwardScanDirection, count=0, execute_once=true) at
execMain.c:363:3
frame #7: 0x0000000106318f11
postgres`ExecutorRun(queryDesc=0x00007faeb280d920,
direction=ForwardScanDirection, count=0, execute_once=true) at
execMain.c:307:3
frame #8: 0x000000010661139c
postgres`PortalRunSelect(portal=0x00007faeb5028920, forward=true,
count=0, dest=0x00007faeb3809390) at pquery.c:924:4
frame #9: 0x0000000106610d3f
postgres`PortalRun(portal=0x00007faeb5028920,
count=9223372036854775807, isTopLevel=true, run_once=true,
dest=0x00007faeb3809390, altdest=0x00007faeb3809390,
qc=0x00007ffee9c31620) at pquery.c:768:18
frame #10: 0x000000010660bb93
postgres`exec_simple_query(query_string="select * from tbl;") at
postgres.c:1246:10
frame #11: 0x000000010660ac2f
postgres`PostgresMain(dbname="postgres", username="masahiko") at
postgres.c:4534:7
frame #12: 0x000000010650d9c6
postgres`BackendRun(port=0x00007faeb3004210) at postmaster.c:4490:2
frame #13: 0x000000010650cf8a
postgres`BackendStartup(port=0x00007faeb3004210) at
postmaster.c:4218:3
frame #14: 0x000000010650bd57 postgres`ServerLoop at postmaster.c:1808:7
frame #15: 0x00000001065094cf postgres`PostmasterMain(argc=5,
argv=0x00007faeb2406320) at postmaster.c:1480:11
frame #16: 0x00000001063b4dcf postgres`main(argc=5,
argv=0x00007faeb2406320) at main.c:197:3
frame #17: 0x00007fff721abcc9 libdyld.dylib`start + 1

(2) SEGV by psql

postgres(1:47762)=# create table tbl (t text encrypted with
(column_encryption_key = cek1));
CREATE TABLE
postgres(1:47762)=# insert into tbl values ('test');
INSERT 0 1
postgres(1:47762)=# select * from tbl;
Segmentation fault: 11 (core dumped)

The backtrace is:

(lldb) bt
* thread #1, stop reason = signal SIGSTOP
* frame #0: 0x00007fff723a1b36
libsystem_platform.dylib`_platform_memmove$VARIANT$Haswell + 566
frame #1: 0x000000010c509a5f
libpq.5.dylib`get_message_auth_tag(md=0x000000010c7f28b8, mac_key="
\x1c,\x98g½ȩ[\x88\x16\x12Kiꔂ\v8g_\x80, mac_key_len=16, encr="test",
encrlen=-12, cekalg=130, md_value="", md_len_p=0x00007ffee380a720,
errmsgp=0x00007ffee380a8c0) at fe-encrypt-openssl.c:316:2
frame #2: 0x000000010c509442
libpq.5.dylib`decrypt_value(res=0x00007fa098504770,
cek=0x00007fa0985045d0, cekalg=130, input="test", inputlen=4,
errmsgp=0x00007ffee380a8c0) at fe-encrypt-openssl.c:429:7
frame #3: 0x000000010c4f3c0c
libpq.5.dylib`pqRowProcessor(conn=0x00007fa098808e00,
errmsgp=0x00007ffee380a8c0) at fe-exec.c:1670:21
frame #4: 0x000000010c5013bb
libpq.5.dylib`getAnotherTuple(conn=0x00007fa098808e00, msgLength=16)
at fe-protocol3.c:882:6
frame #5: 0x000000010c4ffbf2
libpq.5.dylib`pqParseInput3(conn=0x00007fa098808e00) at
fe-protocol3.c:410:11
frame #6: 0x000000010c4f5b65
libpq.5.dylib`parseInput(conn=0x00007fa098808e00) at fe-exec.c:2598:2
frame #7: 0x000000010c4f5c59
libpq.5.dylib`PQgetResult(conn=0x00007fa098808e00) at fe-exec.c:2684:3
frame #8: 0x000000010c401a77
psql`ExecQueryAndProcessResults(query="select * from tbl;",
elapsed_msec=0x00007ffee380ab08, svpt_gone_p=0x00007ffee380aafe,
is_watch=false, opt=0x0000000000000000,
printQueryFout=0x0000000000000000) at common.c:1514:11
frame #9: 0x000000010c402469 psql`SendQuery(query="select * from
tbl;") at common.c:1171:9
frame #10: 0x000000010c41a4dd
psql`MainLoop(source=0x00007fff98ce8d90) at mainloop.c:439:16
frame #11: 0x000000010c426b44 psql`main(argc=3,
argv=0x00007ffee380adc8) at startup.c:462:19
frame #12: 0x00007fff721abcc9 libdyld.dylib`start + 1
frame #13: 0x00007fff721abcc9 libdyld.dylib`start + 1
(lldb) f 1
frame #1: 0x000000010c509a5f
libpq.5.dylib`get_message_auth_tag(md=0x000000010c7f28b8, mac_key="
\x1c,\x98g½ȩ[\x88\x16\x12Kiꔂ\v8g_\x80, mac_key_len=16, encr="test",
encrlen=-12, cekalg=130, md_value="", md_len_p=0x00007ffee380a720,
errmsgp=0x00007ffee380a8c0) at fe-encrypt-openssl.c:316:2
313 #else
314 memcpy(buf, test_A, sizeof(test_A));
315 #endif
-> 316 memcpy(buf + PG_AD_LEN, encr, encrlen);
317 *(int64 *) (buf + PG_AD_LEN + encrlen) =
pg_hton64(PG_AD_LEN * 8);
318
319 if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
(lldb) p encrlen
(int) $0 = -12
(lldb)

Regards,

--
Masahiko Sawada
EDB: https://www.enterprisedb.com/

#24Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#20)
Re: Transparent column encryption

On Mon, Jul 18, 2022 at 3:53 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

Some other products make use of secure enclaves to do computations on
(otherwise) encrypted values on the server. I don't fully know how that
works, but I suspect that asymmetric keys can play a role in that. (I
don't have any immediate plans for that in my patch. It seems to be a
dying technology at the moment.)

Asymmetric keys gives you some more options for how you set up the keys
at the beginning. For example, you create the asymmetric key pair on
the host where your client program that wants access to the encrypted
data will run. You put the private key in an appropriate location for
run time. You send the public key to another host. On that other host,
you create the CEK, encrypt it with the CMK, and then upload it into the
server (CREATE COLUMN ENCRYPTION KEY). Then you can wipe that second
host. That way, you can be even more sure that the unencrypted CEK
isn't left anywhere. I'm not sure whether this method is very useful in
practice, but it's interesting.

As long as it's clear to people trying this that the "public" key
cannot actually be made public, I suppose. That needs to be documented
IMO. I like your idea of providing a symmetric option as well.

In any case, as I mentioned above, this particular aspect is up for
discussion.

Also note that if you use a KMS (cmklookup "run" method), the actual
algorithm doesn't even matter (depending on details of the KMS setup),
since you just tell the KMS "decrypt this", and the KMS knows by itself
what algorithm to use. Maybe there should be a way to specify "unknown"
in the ckdcmkalg field.

+1, an officially client-defined method would probably be useful.

The short answer is, these same algorithms are used in equivalent
products (see MS SQL Server, MongoDB). They even reference the same
exact draft document.

Besides that, here is my analysis for why these are good choices: You
can't use any of the counter modes, because since the encryption happens
on the client, there is no way to coordinate to avoid nonce reuse. So
among mainstream modes, you are basically left with AES-CBC with a
random IV. In that case, even if you happen to reuse an IV, the
possible damage is very contained.)

I think both AES-GCM-SIV and XChaCha20-Poly1305 are designed to handle
the nonce problem as well. In any case, if I were deploying this, I'd
want to know the characteristics/limits of our chosen suites (e.g. how
much data can be encrypted per key) so that I could plan rotations
correctly. Something like the table in [1]https://doc.libsodium.org/secret-key_cryptography/aead?

Since we're requiring "canonical" use of text format, and the docs say
there are no embedded or trailing nulls allowed in text values, could we
steal the use of a single zero byte to mean NULL? One additional
complication would be that the client would have to double-check that
we're not writing a NULL into a NOT NULL column, and complain if it
reads one during decryption. Another complication would be that the
client would need to complain if it got a plaintext NULL.

You're already alluding to some of the complications. Also consider
that null values could arise from, say, outer joins. So you could be in
a situation where encrypted and unencrypted null values coexist.

(I realize I'm about to wade into the pool of what NULL means in SQL,
the subject of which I've stayed mostly, gleefully, ignorant.)

To be honest that sounds pretty useful. Any unencrypted null must have
come from the server computation; it's a clear claim by the server
that no such rows exist. (If the encrypted column is itself NOT NULL
then there's no ambiguity to begin with, I think.) That wouldn't be
transparent behavior anymore, so it may (understandably) be a non-goal
for the patch, but it really does sound useful.

And it might be a little extreme, but if I as a user decided that I
wanted in-band encrypted null, it wouldn't be particularly surprising
to me if such a column couldn't be included in an outer join. Just
like I can't join on a randomized encrypted column, or add two
encrypted NUMERICs to each other. In fact I might even want the server
to enforce NOT NULL transparently on the underlying pg_encrypted_*
column, to make sure that I didn't accidentally push an unencrypted
NULL by mistake.

And of
course the server doesn't know about the encrypted null values. So how
do you maintain semantics, like for aggregate functions, primary keys,
anything that treats null values specially?

Could you elaborate? Any special cases seem like they'd be important
to document regardless of whether or not we support in-band null
encryption. For example, do you plan to support encrypted primary
keys, null or not? That seems like it'd be particularly difficult
during CEK rotation.

How do clients deal with a
mix of encrypted and unencrypted null values, how do they know which one
is real.

That one seems straightforward -- a bare null in an encrypted column
is an assertion by the server. An encrypted null had to have come from
the client side originally.

What if the client needs to send a null value back as a
parameter?

Couldn't the client just encrypt it, same as any other column? Or am I
missing what you mean by "parameter" here?

All of this would create enormous complications, if they can
be solved at all.

That could be. But I'm wondering if the complications exist
regardless, and the null example is just making them more obvious.

It has been recommended that you include the identity of the encryption
algorithm in the AD. This protects the client from having to decrypt
stuff that wasn't meant to be decrypted (in that way).

Do you have a link? I'd like to read up on that -- I naively assumed
that the suite wouldn't be able to decrypt another AEAD cipher without
complaining.

--Jacob

[1]: https://doc.libsodium.org/secret-key_cryptography/aead

#25Jacob Champion
jchampion@timescale.com
In reply to: Robert Haas (#21)
Re: Transparent column encryption

On Mon, Jul 18, 2022 at 9:07 AM Robert Haas <robertmhaas@gmail.com> wrote:

Even there, what can be accomplished with a feature that only encrypts
individual column values is by nature somewhat limited. If you have a
text column that, for one row, stores the value 'a', and for some
other row, stores the entire text of Don Quixote in the original
Spanish, it is going to be really difficult to keep an adversary who
can read from the disk from distinguishing those rows. If you want to
fix that, you're going to need to do block-level encryption or
something of that sort.

A minimum padding option would fix the leak here, right? If every
entry is the same length then there's no information to be gained, at
least in an offline analysis.

I think some work around that is probably going to be needed for
serious use of this encryption, in part because of the use of text
format as the canonical input. If the encrypted values of 1, 10, 100,
and 1000 hypothetically leaked their exact lengths, then an encrypted
int wouldn't be very useful. So I'd want to quantify (and possibly
configure) exactly how much data you can encrypt in a single message
before the length starts being leaked, and then make sure that my
encrypted values stay inside that bound.

--Jacob

#26Bruce Momjian
bruce@momjian.us
In reply to: Peter Eisentraut (#20)
Re: Transparent column encryption

On Mon, Jul 18, 2022 at 12:53:23PM +0200, Peter Eisentraut wrote:

Asymmetric keys gives you some more options for how you set up the keys at
the beginning. For example, you create the asymmetric key pair on the host
where your client program that wants access to the encrypted data will run.
You put the private key in an appropriate location for run time. You send
the public key to another host. On that other host, you create the CEK,
encrypt it with the CMK, and then upload it into the server (CREATE COLUMN
ENCRYPTION KEY). Then you can wipe that second host. That way, you can be
even more sure that the unencrypted CEK isn't left anywhere. I'm not sure
whether this method is very useful in practice, but it's interesting.

In any case, as I mentioned above, this particular aspect is up for
discussion.

I caution against adding complexity without a good reason, because
historically complexity often leads to exploits and bugs, especially
with crypto.

--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com

Indecision is a decision. Inaction is an action. Mark Batterson

#27Robert Haas
robertmhaas@gmail.com
In reply to: Jacob Champion (#25)
Re: Transparent column encryption

On Thu, Jul 21, 2022 at 2:30 PM Jacob Champion <jchampion@timescale.com> wrote:

On Mon, Jul 18, 2022 at 9:07 AM Robert Haas <robertmhaas@gmail.com> wrote:

Even there, what can be accomplished with a feature that only encrypts
individual column values is by nature somewhat limited. If you have a
text column that, for one row, stores the value 'a', and for some
other row, stores the entire text of Don Quixote in the original
Spanish, it is going to be really difficult to keep an adversary who
can read from the disk from distinguishing those rows. If you want to
fix that, you're going to need to do block-level encryption or
something of that sort.

A minimum padding option would fix the leak here, right? If every
entry is the same length then there's no information to be gained, at
least in an offline analysis.

Sure, but padding every text column that you have, even the ones
containing only 'a', out to the length of Don Quixote in the original
Spanish, is unlikely to be an appealing option.

I think some work around that is probably going to be needed for
serious use of this encryption, in part because of the use of text
format as the canonical input. If the encrypted values of 1, 10, 100,
and 1000 hypothetically leaked their exact lengths, then an encrypted
int wouldn't be very useful. So I'd want to quantify (and possibly
configure) exactly how much data you can encrypt in a single message
before the length starts being leaked, and then make sure that my
encrypted values stay inside that bound.

I think most ciphers these days are block ciphers, so you're going to
get output that is a multiple of the block size anyway - e.g. I think
for AES it's 128 bits = 16 bytes. So small differences in length will
be concealed naturally, which may be good enough for some use cases.

I'm not really convinced that it's worth putting a lot of effort into
bolstering the security of this kind of tech above what it naturally
gives. I think it's likely to be a wild goose chase. If you have major
worries about someone reading your disk in its entirety, use full-disk
encryption. Selective encryption is only suitable when you want to add
a modest level of protection for individual value and are willing to
accept that some information leakage is likely if an adversary can in
fact read the full disk. Padding values to try to further obscure
things may be situationally useful, but if you find yourself worrying
too much about that sort of thing, you likely should have picked
stronger medicine initially.

--
Robert Haas
EDB: http://www.enterprisedb.com

#28Jacob Champion
jchampion@timescale.com
In reply to: Robert Haas (#27)
Re: Transparent column encryption

On Tue, Jul 26, 2022 at 10:52 AM Robert Haas <robertmhaas@gmail.com> wrote:

On Thu, Jul 21, 2022 at 2:30 PM Jacob Champion <jchampion@timescale.com> wrote:

A minimum padding option would fix the leak here, right? If every
entry is the same length then there's no information to be gained, at
least in an offline analysis.

Sure, but padding every text column that you have, even the ones
containing only 'a', out to the length of Don Quixote in the original
Spanish, is unlikely to be an appealing option.

If you are honestly trying to conceal Don Quixote, I suspect you are
already in the business of making unappealing decisions. I don't think
that's necessarily an argument against hiding the length for
real-world use cases.

I think some work around that is probably going to be needed for
serious use of this encryption, in part because of the use of text
format as the canonical input. If the encrypted values of 1, 10, 100,
and 1000 hypothetically leaked their exact lengths, then an encrypted
int wouldn't be very useful. So I'd want to quantify (and possibly
configure) exactly how much data you can encrypt in a single message
before the length starts being leaked, and then make sure that my
encrypted values stay inside that bound.

I think most ciphers these days are block ciphers, so you're going to
get output that is a multiple of the block size anyway - e.g. I think
for AES it's 128 bits = 16 bytes. So small differences in length will
be concealed naturally, which may be good enough for some use cases.

Right. My point is, if you have a column that has exactly one
important value that is 17 bytes long when converted to text, you're
going to want to know that block size exactly, because the encryption
will be effectively useless for that value. That size needs to be
documented, and it'd be helpful to know that it's longer than, say,
the longest text representation of our fixed-length column types.

I'm not really convinced that it's worth putting a lot of effort into
bolstering the security of this kind of tech above what it naturally
gives. I think it's likely to be a wild goose chase.

If the goal is to provide real encryption, and not just a toy, I think
you're going to need to put a *lot* of effort into analysis. Even if
the result of the analysis is "we don't plan to address this in v1".

Crypto is inherently a cycle of
make-it-and-break-it-and-fix-it-and-break-it-again. If that's
considered a "wild goose chase" and not seriously pursued at some
level, then this implementation will probably not last long in the
face of real abuse. (That doesn't mean you have to take my advice; I'm
just a dude with opinions -- but you will need to have real
cryptographers look at this, and you're going to need to think about
how the system will evolve when it's broken.)

If you have major
worries about someone reading your disk in its entirety, use full-disk
encryption.

This patchset was designed to protect against the evil DBA case, I
think. Full disk encryption doesn't help.

Selective encryption is only suitable when you want to add
a modest level of protection for individual value and are willing to
accept that some information leakage is likely if an adversary can in
fact read the full disk.

...but there's a known countermeasure to this particular leakage,
right? Which would make it more suitable for that case.

Padding values to try to further obscure
things may be situationally useful, but if you find yourself worrying
too much about that sort of thing, you likely should have picked
stronger medicine initially.

In my experience, this entire field is the application of
situationally useful protection. That's one of the reasons it's hard,
and designing this sort of patch is going to be hard too. Putting that
on the user isn't quite fair when you're the ones designing the
system; you determine what they have to worry about when you choose
the crypto.

--Jacob

#29Robert Haas
robertmhaas@gmail.com
In reply to: Jacob Champion (#28)
Re: Transparent column encryption

On Tue, Jul 26, 2022 at 2:27 PM Jacob Champion <jchampion@timescale.com> wrote:

Right. My point is, if you have a column that has exactly one
important value that is 17 bytes long when converted to text, you're
going to want to know that block size exactly, because the encryption
will be effectively useless for that value. That size needs to be
documented, and it'd be helpful to know that it's longer than, say,
the longest text representation of our fixed-length column types.

I certainly have no objection to being clear about such details in the
documentation.

If the goal is to provide real encryption, and not just a toy, I think
you're going to need to put a *lot* of effort into analysis. Even if
the result of the analysis is "we don't plan to address this in v1".

Crypto is inherently a cycle of
make-it-and-break-it-and-fix-it-and-break-it-again. If that's
considered a "wild goose chase" and not seriously pursued at some
level, then this implementation will probably not last long in the
face of real abuse. (That doesn't mean you have to take my advice; I'm
just a dude with opinions -- but you will need to have real
cryptographers look at this, and you're going to need to think about
how the system will evolve when it's broken.)

Well, I'm just a dude with opinions, too. I fear the phenomenon where
doing anything about a problem makes you responsible for the whole
problem. If we disclaim the ability to hide the length of values,
that's clear enough. But if we start padding to try to hide the length
of values, then people might expect it to work in all cases, and I
don't see how it ever can. Moreover, I think that the padding might
need to be done in a "cryptographically intelligent" way rather than
just, say, adding trailing blanks. Now that being said, if Peter wants
to implement something around padding that he has reason to believe
will not create cryptographic weaknesses, I have no issue with that. I
just don't view it as an essential part of the feature, because hiding
such things doesn't seem like it can ever be the main point of a
feature like this.

Padding values to try to further obscure
things may be situationally useful, but if you find yourself worrying
too much about that sort of thing, you likely should have picked
stronger medicine initially.

In my experience, this entire field is the application of
situationally useful protection. That's one of the reasons it's hard,
and designing this sort of patch is going to be hard too.

Agreed.

Putting that
on the user isn't quite fair when you're the ones designing the
system; you determine what they have to worry about when you choose
the crypto.

I guess my view on this is that, if you're trying to hide something
like a credit card number, most likely every value in the system is
the same length, and then this is a non-issue. On the other hand, if
the secret column is a person's name, then there is an issue, but
you're not going to pad every value out the maximum length of a
varlena, so you have to make an estimate of how long a name someone
might reasonably have to decide how much padding to include. You also
have to decide whether the storage cost of padding every value is
worth it to you given the potential information leakage. Only the
human user can make those decisions, so some amount of "putting that
on the user" feels inevitable. Now, if we don't have a padding system
built into the feature, then that does put even more on the user; it's
hard to argue with that.

--
Robert Haas
EDB: http://www.enterprisedb.com

#30Jacob Champion
jchampion@timescale.com
In reply to: Robert Haas (#29)
Re: Transparent column encryption

On 7/26/22 13:25, Robert Haas wrote:

I certainly have no objection to being clear about such details in the
documentation.

Cool.

I fear the phenomenon where
doing anything about a problem makes you responsible for the whole
problem. If we disclaim the ability to hide the length of values,
that's clear enough.

I don't think disclaiming responsibility absolves you of it here, in
part because choices are being made (text format) that make length
hiding even more important than it otherwise would be. A user who
already knows that encryption doesn't hide length might still reasonably
expect a fixed-length column type like bigint to be protected in all
cases. It won't be (at least not with your 16-byte example).

And sure, you can document that caveat too, but said user might then
reasonably wonder how they're supposed to actually make it safe.

But if we start padding to try to hide the length
of values, then people might expect it to work in all cases, and I
don't see how it ever can.

Well, that's where I agree with you on the value of solid documentation.
But there are other things we can do as well. In general we should

- choose a default that will protect most people out of the box,
- document the heck out of the default's limitations,
- provide guardrails that warn the user when they're outgrowing those
limitations, and
- give people a way to tune it to their own use cases.

As an example, a naive guardrail in this instance could be to simply
have the client refuse to encrypt data past the padding maximum, if
you've gone so far as to set one up. It'd suck to hit that maximum in
production and have to rewrite the column, but you did want your
encryption to hide your data, right?

Maybe that's way too complex to think about for a v1, but it'll be
easier to maintain this into the future if there's at least a plan to
create a v2. If you declare it out of scope, instead of considering it a
potential TODO, then I think it'll be a lot harder for people to improve it.

Moreover, I think that the padding might
need to be done in a "cryptographically intelligent" way rather than
just, say, adding trailing blanks.

Possibly. I think that's where AEAD comes in -- if you've authenticated
your ciphertext sufficiently, padding oracles should be prohibitively
difficult(?). (But see below; I think we also have other things to worry
about in terms of authentication and oracles.)

Now that being said, if Peter wants
to implement something around padding that he has reason to believe
will not create cryptographic weaknesses, I have no issue with that. I
just don't view it as an essential part of the feature, because hiding
such things doesn't seem like it can ever be the main point of a
feature like this.

I think that side channel consideration has to be an essential part of
any cryptography feature. Recent history has shown "obscure" side
channels gaining power to the point of completely breaking crypto schemes.

And it's not like TLS where we have to protect an infinite stream of
arbitrary bytes; this is going to be used on small values that probably
get repeated often and have (comparatively) very little entropy.
Cryptanalysis based on length seems to me like part and parcel of the
problem space.

I guess my view on this is that, if you're trying to hide something
like a credit card number, most likely every value in the system is
the same length, and then this is a non-issue.

Agreed.

On the other hand, if
the secret column is a person's name, then there is an issue, but
you're not going to pad every value out the maximum length of a
varlena, so you have to make an estimate of how long a name someone
might reasonably have to decide how much padding to include. You also
have to decide whether the storage cost of padding every value is
worth it to you given the potential information leakage. Only the
human user can make those decisions, so some amount of "putting that
on the user" feels inevitable.

Agreed.

Now, if we don't have a padding system
built into the feature, then that does put even more on the user; it's
hard to argue with that.

Right. If they can even fix it at all. Having a well-documented padding
feature would not only help mitigate that, it would conveniently hang a
big sign on the caveats that exist.

--

Speaking of oracles and side channels. Users may want to use associated
data to further lock an encrypted value to its column type, too.
Otherwise it seems like an active DBA could feed an encrypted text blob
to a client in place of, say, an int column, to see whether or not that
text blob is a number. Seems like AD is going to be important to prevent
active attacks in general.

--Jacob

#31Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#22)
1 attachment(s)
Re: Transparent column encryption

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.)

Attachments:

v6-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v6-0001-Transparent-column-encryption.patchDownload
From 7ef2058799b29860b8a97c9a35bb16b0e0f5758d Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Tue, 30 Aug 2022 13:11:17 +0200
Subject: [PATCH v6] 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/test/Makefile                             |   3 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  29 +
 .../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 +
 .../regress/expected/column_encryption.out    | 115 +++
 src/test/regress/expected/object_address.out  |  47 +-
 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    | 107 +++
 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 +
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/dependency.h              |   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                |  16 +
 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                 |   5 +
 src/include/utils/plancache.h                 |   6 +
 src/include/utils/syscache.h                  |   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                  |  62 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |   3 +
 src/backend/catalog/objectaddress.c           | 183 +++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  15 +
 src/backend/commands/colenccmds.c             | 361 +++++++++
 src/backend/commands/event_trigger.c          |   9 +
 src/backend/commands/prepare.c                |  74 +-
 src/backend/commands/seclabel.c               |   2 +
 src/backend/commands/tablecmds.c              |  83 ++
 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                     | 119 ++-
 src/backend/parser/parse_param.c              |  43 +-
 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           |  93 +++
 src/backend/utils/cache/plancache.c           |  10 +
 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/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                | 592 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 180 +++-
 src/interfaces/libpq/fe-trace.c               |  31 +-
 src/interfaces/libpq/libpq-fe.h               |  22 +
 src/interfaces/libpq/libpq-int.h              |  37 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 .../postgres_fdw/expected/postgres_fdw.out    |   2 +-
 130 files changed, 7395 insertions(+), 145 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/test/column_encryption/.gitignore
 create mode 100644 src/test/column_encryption/Makefile
 create mode 100644 src/test/column_encryption/t/001_column_encryption.pl
 create mode 100644 src/test/column_encryption/t/002_cmk_rotation.pl
 create mode 100644 src/test/column_encryption/test_client.c
 create mode 100755 src/test/column_encryption/test_run_decrypt.pl
 create mode 100644 src/test/regress/expected/column_encryption.out
 create mode 100644 src/test/regress/sql/column_encryption.sql
 create mode 100644 src/include/catalog/pg_colenckey.h
 create mode 100644 src/include/catalog/pg_colenckeydata.h
 create mode 100644 src/include/catalog/pg_colmasterkey.h
 create mode 100644 src/include/commands/colenccmds.h
 create mode 100644 src/backend/commands/colenccmds.c
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h

diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 2df6559acc..bd1a0185ed 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 00f833d210..638be1c850 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 4cc9e59270..f643668f4f 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5344,4 +5344,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 3717d13fff..06c0d9b4f5 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 d6d0a3a814..10507e4391 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 8a1a9e9932..7968630aa0 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.
@@ -4686,6 +4903,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>
 
@@ -4736,6 +4993,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"/>
@@ -7766,6 +8024,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 87870c5b10..f421ca0863 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>
 
@@ -4049,6 +4111,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>
@@ -5145,6 +5341,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>
 
@@ -5533,6 +5769,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>
 
@@ -7336,6 +7601,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 e90a0e1f83..331b1f010b 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 0000000000..7597cd80ca
--- /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 0000000000..13e310b227
--- /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 0000000000..a94b0924b1
--- /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 0000000000..ec5fa4cde5
--- /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 c14b2010d8..feebf51fd9 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 0000000000..9d157de9c5
--- /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 0000000000..c85098ea1c
--- /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 c08276bc0a..83aab1cf0e 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 5d54074e01..bdde5fc8c1 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 186f8c506a..f4b955a38c 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 a3b743e8c1..e1425c222f 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/test/Makefile b/src/test/Makefile
index 69ef074d75..2300be8332 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -32,6 +32,7 @@ SUBDIRS += ldap
 endif
 endif
 ifeq ($(with_ssl),openssl)
+SUBDIRS += column_encryption
 ifneq (,$(filter ssl,$(PG_TEST_EXTRA)))
 SUBDIRS += ssl
 endif
@@ -41,7 +42,7 @@ endif
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl column_encryption)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..7aca84b17a
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,29 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/column_encryption/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..4a23414ee0
--- /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 0000000000..ec447872ab
--- /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 0000000000..636de88c78
--- /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 0000000000..9664349f14
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,41 @@
+#!/usr/bin/perl
+
+# Test/sample command for libpq cmklookup run scheme
+#
+# This just places the data into temporary files and runs the openssl
+# command on it.  (In practice, this could more simply be written as a
+# shell script, but this way it's more portable.)
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use MIME::Base64;
+
+my ($tmpdir, $cmkname, $alg, $b64data) = @ARGV;
+
+die unless $alg eq 'RSAES_OAEP_SHA_1';
+
+open my $fh, '>:raw', "${tmpdir}/input.tmp" or die;
+print $fh decode_base64($b64data);
+close $fh;
+
+system('openssl', 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', "${tmpdir}/input.tmp", '-out', "${tmpdir}/output.tmp") == 0 or die;
+
+open $fh, '<:raw', "${tmpdir}/output.tmp" or die;
+my $data = '';
+
+while (1) {
+	my $success = read $fh, $data, 100, length($data);
+	die $! if not defined $success;
+	last if not $success;
+}
+
+close $fh;
+
+unlink "${tmpdir}/input.tmp", "${tmpdir}/output.tmp";
+
+print encode_base64($data);
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..a31e5391cd
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,115 @@
+\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'
+);
+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       |              | 
+
+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 4117fc27c9..b43e24a9a1 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 relation')
+		('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
@@ -319,6 +322,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},{}: unsupported object type "column encryption key data"
+WARNING:  error for column encryption key data,{eins},{integer}: unsupported object type "column encryption key data"
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{}: unsupported object type "column encryption key data"
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: unsupported object type "column encryption key data"
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: unsupported object type "column encryption key data"
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: unsupported object type "column encryption key data"
+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
@@ -374,6 +395,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[]),
@@ -396,6 +425,9 @@ WITH objects (type, name, args) AS (VALUES
 				('type', '{addr_nsp.genenum}', '{}'),
 				('cast', '{int8}', '{int4}'),
 				('collation', '{default}', '{}'),
+				('column encryption key', '{addr_cek}', '{}'),
+--TODO:				('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}', '{}'),
@@ -493,7 +525,9 @@ SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*,
  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)
+ column master key         |            | addr_cmk          | addr_cmk                                                             | t
+ column encryption key     |            | addr_cek          | addr_cek                                                             | t
+(52 rows)
 
 ---
 --- Cleanup resources
@@ -507,6 +541,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
@@ -542,6 +578,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
+--TODO:    ('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
@@ -572,6 +611,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
@@ -623,5 +663,8 @@ 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
 -- restore normal output mode
 \a\t
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..2aa0e16323 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attrealtypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,7 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 86d755aa44..4a366074da 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -175,13 +175,14 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  bigint                      | xid8
  text                        | character
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
-(6 rows)
+(7 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +198,13 @@ WHERE p1.oid != p2.oid AND
 ORDER BY 1, 2;
          proargtypes         |       proargtypes        
 -----------------------------+--------------------------
+ bytea                       | pg_encrypted_det
  integer                     | xid
  timestamp without time zone | timestamp with time zone
  bit                         | bit varying
  txid_snapshot               | pg_snapshot
  anyrange                    | anymultirange
-(5 rows)
+(6 rows)
 
 SELECT DISTINCT p1.proargtypes[2]::regtype, p2.proargtypes[2]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -841,6 +843,8 @@ xid8ge(xid8,xid8)
 xid8eq(xid8,xid8)
 xid8ne(xid8,xid8)
 xid8cmp(xid8,xid8)
+pg_encrypted_det_eq(pg_encrypted_det,pg_encrypted_det)
+pg_encrypted_det_ne(pg_encrypted_det,pg_encrypted_det)
 -- restore normal output mode
 \a\t
 -- List of functions used by libpq's fe-lobj.c
@@ -988,7 +992,9 @@ WHERE c.castmethod = 'b' AND
  xml               | text              |        0 | a
  xml               | character varying |        0 | a
  xml               | character         |        0 | a
-(10 rows)
+ bytea             | pg_encrypted_det  |        0 | e
+ bytea             | pg_encrypted_rnd  |        0 | e
+(12 rows)
 
 -- **************** pg_conversion ****************
 -- Look for illegal values in pg_conversion fields.
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39..4482a65d24 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 a7f5700edc..2ee5f7546d 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 7ec3d2688f..42e38d66d1 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 d3ac08c9ee..357095e1b9 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 103e11483d..ffca206c6f 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -129,6 +129,9 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # ----------
 test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats
 
+# WIP
+test: column_encryption
+
 # event_trigger cannot run concurrently with any test that runs DDL
 # oidjoins is read-only, though, and should run late for best coverage
 test: event_trigger oidjoins
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index a4b354c9e6..8ad1f458d5 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..ed4b7c5d63
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,107 @@
+\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'
+);
+
+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
+
+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 acd0468a9d..8f4fa6e21c 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 relation')
+		('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}', '{}'),
+--TODO:				('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
+--TODO:    ('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 c6098dc95c..7db788735c 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 1149c6a839..e88c70d302 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -119,6 +119,23 @@ CREATE TABLE bububu(a int) \gdesc
 -- all on one line
 SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g
 
+
+-- \gencr
+-- (This just tests the parameter passing; there is no encryption here.)
+
+CREATE TABLE test_gencr (a int, b text);
+INSERT INTO test_gencr VALUES (1, 'one') \gencr
+SELECT * FROM test_gencr WHERE a = 1 \gencr
+
+INSERT INTO test_gencr VALUES ($1, $2) \gencr 2 'two'
+SELECT * FROM test_gencr WHERE a IN ($1, $2) \gencr 2 3
+
+-- test parse error
+SELECT * FROM test_gencr WHERE a = x \gencr
+-- test bind error
+SELECT * FROM test_gencr WHERE a = $1 \gencr
+
+
 -- \gexec
 
 create temporary table gexec_test(a int, b text, c date, d float);
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 5edc1f1f6e..0ffe45fd37 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,
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 971a74cf22..db1f3b8811 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,6 +20,8 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 729c4c46c0..e237155971 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/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 61cd591430..a0bd708132 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4cc129bebd..dddb27113f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 053294c99f..550a53649b 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,15 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	bool		attislocal BKI_DEFAULT(t);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey);
+
+	/* real type if encrypted */
+	Oid			attrealtypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+
+	/* encryption algorithm (PG_CEK_* values) */
+	int16		attencalg BKI_DEFAULT(0);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index 4471eb6bbe..878b0bcbbf 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -546,4 +546,10 @@
 { castsource => 'tstzrange', casttarget => 'tstzmultirange',
   castfunc => 'tstzmultirange(tstzrange)', castcontext => 'e',
   castmethod => 'f' },
+
+{ castsource => 'bytea', casttarget => 'pg_encrypted_det', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+{ castsource => 'bytea', casttarget => 'pg_encrypted_rnd', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+
 ]
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..095ed712b0
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,55 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+/* ----------------
+ *		pg_colenckey definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckey
+ * ----------------
+ */
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_index, 8242, ColumnEncKeyNameIndexId, on pg_colenckey using btree(cekname name_ops));
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  For clarity, the assigned numbers are not reused between CMKs
+ * and CEKs, but that is not technically required.  In either case, don't
+ * assign zero, so that that can be used as an invalid value.
+ */
+
+#define PG_CMK_RSAES_OAEP_SHA_1			1
+#define PG_CMK_RSAES_OAEP_SHA_256		2
+
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	130
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	131
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	132
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	133
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..3e4dea7218
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkeydata.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+/* ----------------
+ *		pg_colenckeydata definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckeydata
+ * ----------------
+ */
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+	int16		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..0344cc4201
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,45 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_index, 8241, ColumnMasterKeyNameIndexId, on pg_colmasterkey using btree(cmkname name_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index dbcae7ffdd..0ca401ffe4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index bc5f8213f3..4737a7f9ed 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index b3b6a7e616..5343580b06 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be47583122..fefb4b9474 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',
@@ -11894,4 +11894,11 @@
   prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary',
   prosrc => 'brin_minmax_multi_summary_send' },
 
+{ oid => '8245',
+  proname => 'pg_encrypted_det_eq', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteaeq' },
+{ oid => '8246',
+  proname => 'pg_encrypted_det_ne', proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'pg_encrypted_det pg_encrypted_det', prosrc => 'byteane' },
+
 ]
diff --git a/src/include/catalog/pg_type.dat b/src/include/catalog/pg_type.dat
index df45879463..851063cf5f 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 48a2559137..c1d30903b5 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 0000000000..d9741e100b
--- /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 6d452ec6d9..e32c7779c9 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 469a5c46f6..ca1b59583d 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? */
@@ -2151,6 +2152,8 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2343,6 +2346,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 dc379547c7..c310e124f1 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -28,7 +28,9 @@ extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook;
 extern Query *parse_analyze_fixedparams(RawStmt *parseTree, const char *sourceText,
 										const Oid *paramTypes, int numParams, QueryEnvironment *queryEnv);
 extern Query *parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
-									  Oid **paramTypes, int *numParams, QueryEnvironment *queryEnv);
+									  Oid **paramTypes, int *numParams,
+									  Oid **paramOrigTbls, AttrNumber **paramOrigCols,
+									  QueryEnvironment *queryEnv);
 extern Query *parse_analyze_withcb(RawStmt *parseTree, const char *sourceText,
 								   ParserSetupHook parserSetup,
 								   void *parserSetupArg,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index ae35f03251..13ccadfd5d 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -152,6 +152,7 @@ PG_KEYWORD("empty", EMPTY_P, UNRESERVED_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("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -268,6 +269,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 962ebf65de..f6a7178766 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -93,6 +93,7 @@ typedef Node *(*ParseParamRefHook) (ParseState *pstate, ParamRef *pref);
 typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param,
 								  Oid targetTypeId, int32 targetTypeMod,
 								  int location);
+typedef void (*ParamAssignOrigHook) (ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 
 
 /*
@@ -222,6 +223,7 @@ struct ParseState
 	PostParseColumnRefHook p_post_columnref_hook;
 	ParseParamRefHook p_paramref_hook;
 	CoerceParamHook p_coerce_param_hook;
+	ParamAssignOrigHook p_param_assign_orig_hook;
 	void	   *p_ref_hook_state;	/* common passthrough link for above */
 };
 
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index df1ee660d8..da202b28a7 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -18,7 +18,8 @@
 extern void setup_parse_fixed_parameters(ParseState *pstate,
 										 const Oid *paramTypes, int numParams);
 extern void setup_parse_variable_parameters(ParseState *pstate,
-											Oid **paramTypes, int *numParams);
+											Oid **paramTypes, int *numParams,
+											Oid **paramOrigTbls, AttrNumber **paramOrigCols);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f..d82a2e1171 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 70d9dab25b..3038ceb522 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -53,6 +53,8 @@ extern List *pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 											  const char *query_string,
 											  Oid **paramTypes,
 											  int *numParams,
+											  Oid **paramOrigTbls,
+											  AttrNumber **paramOrigCols,
 											  QueryEnvironment *queryEnv);
 extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree,
 										   const char *query_string,
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 3d6411197c..bdb6d741d5 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 50f0288305..064b9cd338 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,10 @@ 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_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 0499635f59..9a7cf794ce 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 4463ea66be..489c6ee734 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/backend/access/common/printsimple.c b/src/backend/access/common/printsimple.c
index c99ae54cb0..86482eb4c8 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 d2f3b57288..80ab5e875b 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 d6fb261e20..e3ecc64ea7 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attrealtypid != attr2->attrealtypid)
+			return false;
+		if (attr1->attencalg != attr2->attencalg)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 10bf26ce7c..cea91d4a88 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 89a0221ec9..7e1a91b974 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 17ff617fba..4a3a62bfd8 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"
@@ -3579,6 +3581,8 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEK:
+					case OBJECT_CMK:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3609,6 +3613,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;
@@ -5582,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 39768fa22b..6f1ea268f0 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 9b03579e6e..e91b0b806d 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -746,6 +746,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attrealtypid - 1] = ObjectIdGetDatum(attrs->attrealtypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attencalg - 1] = Int16GetDatum(attrs->attencalg);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 798c1a2d1e..7d9801d645 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", -1
+	},
+	/* 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:
@@ -1294,6 +1353,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);
@@ -2322,6 +2391,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:
@@ -2629,6 +2700,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);
@@ -3089,6 +3170,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;
@@ -4513,6 +4636,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);
@@ -4978,6 +5113,54 @@ 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:
+			elog(ERROR, "TODO");
+			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 48f7348f91..69f6175c60 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 55219bb097..6caa21e325 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 0000000000..c35125f65f
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,361 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 = GetSysCacheOid2(CEKDATACEKCMK, Anum_pg_colenckeydata_oid,
+									 ObjectIdGetDatum(cekoid),
+									 ObjectIdGetDatum(cmkoid));
+		if (!OidIsValid(cekdataoid))
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("column encryption key \"%s\" has no data for master key \"%s\"",
+							get_cek_name(cekoid, false), get_cmk_name(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);
+		// TODO: check for duplicates
+		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 635d05405e..f008eae6f8 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,8 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -1027,6 +1029,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 +2061,8 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2139,6 +2146,8 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index 579825c159..0f2ed67a96 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -50,7 +50,6 @@ static void InitQueryHashTable(void);
 static ParamListInfo EvaluateParams(ParseState *pstate,
 									PreparedStatement *pstmt, List *params,
 									EState *estate);
-static Datum build_regtype_array(Oid *param_types, int num_params);
 
 /*
  * Implements the 'PREPARE' utility statement.
@@ -62,6 +61,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	RawStmt    *rawstmt;
 	CachedPlanSource *plansource;
 	Oid		   *argtypes = NULL;
+	Oid		   *argorigtbls = NULL;
+	AttrNumber *argorigcols = NULL;
 	int			nargs;
 	List	   *query_list;
 
@@ -108,6 +109,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 
 			argtypes[i++] = toid;
 		}
+
+		argorigtbls = (Oid *) palloc0(nargs * sizeof(Oid));
+		argorigcols = (AttrNumber *) palloc0(nargs * sizeof(AttrNumber));
 	}
 
 	/*
@@ -117,7 +121,9 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 	 * Rewrite the query. The result could be 0, 1, or many queries.
 	 */
 	query_list = pg_analyze_and_rewrite_varparams(rawstmt, pstate->p_sourcetext,
-												  &argtypes, &nargs, NULL);
+												  &argtypes, &nargs,
+												  &argorigtbls, &argorigcols,
+												  NULL);
 
 	/* Finish filling in the CachedPlanSource */
 	CompleteCachedPlan(plansource,
@@ -125,6 +131,8 @@ PrepareQuery(ParseState *pstate, PrepareStmt *stmt,
 					   NULL,
 					   argtypes,
 					   nargs,
+					   argorigtbls,
+					   argorigcols,
 					   NULL,
 					   NULL,
 					   CURSOR_OPT_PARALLEL_OK,	/* allow parallel mode */
@@ -683,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 = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_types[i]);
+			values[3] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGTYPEOID));
+
+			tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = ObjectIdGetDatum(prep_stmt->plansource->param_origtbls[i]);
+			values[4] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, REGCLASSOID));
+
+			tmp_ary = (Datum *) palloc(num_params * sizeof(Datum));
+			for (int i = 0; i < num_params; i++)
+				tmp_ary[i] = Int16GetDatum(prep_stmt->plansource->param_origcols[i]);
+			values[5] = PointerGetDatum(construct_array_builtin(tmp_ary, num_params, INT2OID));
+
 			if (result_desc)
 			{
-				Oid		   *result_types;
-
-				result_types = (Oid *) palloc(result_desc->natts * sizeof(Oid));
+				tmp_ary = (Datum *) palloc(result_desc->natts * sizeof(Datum));
 				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 = (Datum *) palloc(num_params * sizeof(Datum));
-
-	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 7ae19b98bb..9ccdf7616c 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,8 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		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 dacc989d85..ec6af1ca13 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"
@@ -935,6 +936,82 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			ListCell   *lc;
+			char	   *cek = NULL;
+			Oid			cekoid;
+			bool		encdet = false;
+			int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+			foreach(lc, colDef->encryption)
+			{
+				DefElem    *el = lfirst_node(DefElem, lc);
+
+				if (strcmp(el->defname, "column_encryption_key") == 0)
+					cek = strVal(linitial(castNode(TypeName, el->arg)->names));
+				else if (strcmp(el->defname, "encryption_type") == 0)
+				{
+					char	   *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+					if (strcmp(val, "deterministic") == 0)
+						encdet = true;
+					else if (strcmp(val, "randomized") == 0)
+						encdet = false;
+					else
+						ereport(ERROR,
+								errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								errmsg("unrecognized encryption type: %s", val));
+				}
+				else if (strcmp(el->defname, "algorithm") == 0)
+				{
+					char	   *val = strVal(el->arg);
+
+					if (strcmp(val, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+						alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+					else if (strcmp(val, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+						alg = PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+					else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+						alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+					else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+						alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+					else
+						ereport(ERROR,
+								errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								errmsg("unrecognized encryption algorithm: %s", val));
+				}
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("unrecognized column encryption parameter: %s", el->defname));
+			}
+
+			if (!cek)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+						errmsg("column encryption key must be specified"));
+
+			cekoid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid, PointerGetDatum(cek));
+			if (!cekoid)
+				ereport(ERROR,
+						errcode(ERRCODE_UNDEFINED_OBJECT),
+						errmsg("column encryption key \"%s\" does not exist", cek));
+
+			attr->attcek = cekoid;
+			attr->attrealtypid = attr->atttypid;
+			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;
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -6868,6 +6945,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attgenerated = colDef->generated;
 	attribute.attisdropped = false;
 	attribute.attislocal = colDef->is_local;
+	attribute.attcek = 0; // TODO
+	attribute.attrealtypid = 0; // TODO
+	attribute.attencalg = 0; // TODO
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
 
@@ -12652,6 +12732,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:
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index e5ddcda0b4..74e46cc73e 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -24,6 +24,7 @@
 #include "access/xlog.h"
 #include "catalog/pg_authid.h"
 #include "commands/variable.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "utils/acl.h"
@@ -648,7 +649,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 29bc26669b..cde5b0e087 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2279,6 +2279,8 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan)
 						   NULL,
 						   plan->argtypes,
 						   plan->nargs,
+						   NULL,
+						   NULL,
 						   plan->parserSetup,
 						   plan->parserSetupArg,
 						   plan->cursor_options,
@@ -2516,6 +2518,8 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 							   NULL,
 							   plan->argtypes,
 							   plan->nargs,
+							   NULL,
+							   NULL,
 							   plan->parserSetup,
 							   plan->parserSetupArg,
 							   plan->cursor_options,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index c334daae39..de52fea522 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4294,6 +4294,8 @@ raw_expression_tree_walker(Node *node,
 					return true;
 				if (walker(coldef->compression, context))
 					return true;
+				if (walker(coldef->encryption, context))
+					return true;
 				if (walker(coldef->raw_default, context))
 					return true;
 				if (walker(coldef->collClause, context))
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 6688c2a865..ce9b7a5f5f 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 b5ab9d9c9a..485924e1af 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -291,7 +291,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
@@ -431,6 +431,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
@@ -604,6 +605,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
@@ -797,7 +799,7 @@ 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 EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
+	EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTION ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
 	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
@@ -822,7 +824,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 NESTED NEW NEXT NFC NFD NFKC NFKD NO
@@ -1061,6 +1063,7 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -3799,14 +3802,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;
@@ -3815,8 +3819,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;
@@ -3873,6 +3877,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 		;
@@ -6368,6 +6377,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; }
@@ -6387,6 +6414,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)); }
@@ -6922,6 +6953,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; }
@@ -9238,6 +9271,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);
@@ -10246,6 +10299,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);
@@ -11402,6 +11473,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
@@ -17746,6 +17845,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ERROR_P
 			| ESCAPE
@@ -17813,6 +17913,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -18317,6 +18418,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ERROR_P
@@ -18420,6 +18522,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 f668abfcb3..b45bf4c438 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -49,6 +49,8 @@ typedef struct VarParamState
 {
 	Oid		  **paramTypes;		/* array of parameter type OIDs */
 	int		   *numParams;		/* number of array entries */
+	Oid		  **paramOrigTbls;	/* underlying tables (0 if none) */
+	AttrNumber **paramOrigCols; /* underlying columns (0 if none) */
 } VarParamState;
 
 static Node *fixed_paramref_hook(ParseState *pstate, ParamRef *pref);
@@ -56,6 +58,7 @@ static Node *variable_paramref_hook(ParseState *pstate, ParamRef *pref);
 static Node *variable_coerce_param_hook(ParseState *pstate, Param *param,
 										Oid targetTypeId, int32 targetTypeMod,
 										int location);
+static void variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 static bool check_parameter_resolution_walker(Node *node, ParseState *pstate);
 static bool query_contains_extern_params_walker(Node *node, void *context);
 
@@ -81,15 +84,19 @@ setup_parse_fixed_parameters(ParseState *pstate,
  */
 void
 setup_parse_variable_parameters(ParseState *pstate,
-								Oid **paramTypes, int *numParams)
+								Oid **paramTypes, int *numParams,
+								Oid **paramOrigTbls, AttrNumber **paramOrigCols)
 {
 	VarParamState *parstate = palloc(sizeof(VarParamState));
 
 	parstate->paramTypes = paramTypes;
 	parstate->numParams = numParams;
+	parstate->paramOrigTbls = paramOrigTbls;
+	parstate->paramOrigCols = paramOrigCols;
 	pstate->p_ref_hook_state = (void *) parstate;
 	pstate->p_paramref_hook = variable_paramref_hook;
 	pstate->p_coerce_param_hook = variable_coerce_param_hook;
+	pstate->p_param_assign_orig_hook = variable_param_assign_orig_hook;
 }
 
 /*
@@ -145,14 +152,36 @@ variable_paramref_hook(ParseState *pstate, ParamRef *pref)
 	{
 		/* Need to enlarge param array */
 		if (*parstate->paramTypes)
+		{
 			*parstate->paramTypes = (Oid *) repalloc(*parstate->paramTypes,
 													 paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc(*parstate->paramOrigTbls,
+													paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc(*parstate->paramOrigCols,
+													paramno * sizeof(AttrNumber));
+		}
 		else
+		{
 			*parstate->paramTypes = (Oid *) palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc(paramno * sizeof(Oid));
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc(paramno * sizeof(AttrNumber));
+		}
 		/* Zero out the previously-unreferenced slots */
 		MemSet(*parstate->paramTypes + *parstate->numParams,
 			   0,
 			   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigTbls)
+			MemSet(*parstate->paramOrigTbls + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(Oid));
+		if (parstate->paramOrigCols)
+			MemSet(*parstate->paramOrigCols + *parstate->numParams,
+				   0,
+				   (paramno - *parstate->numParams) * sizeof(AttrNumber));
 		*parstate->numParams = paramno;
 	}
 
@@ -260,6 +289,18 @@ variable_coerce_param_hook(ParseState *pstate, Param *param,
 	return NULL;
 }
 
+static void
+variable_param_assign_orig_hook(ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol)
+{
+	VarParamState *parstate = (VarParamState *) pstate->p_ref_hook_state;
+	int			paramno = param->paramid;
+
+	if (parstate->paramOrigTbls)
+		(*parstate->paramOrigTbls)[paramno - 1] = origtbl;
+	if (parstate->paramOrigCols)
+		(*parstate->paramOrigCols)[paramno - 1] = origcol;
+}
+
 /*
  * Check for consistent assignment of variable parameters after completion
  * of parsing with parse_variable_parameters.
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 16a0fe59e2..3534683cb0 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -595,6 +595,12 @@ transformAssignedExpr(ParseState *pstate,
 					 parser_errposition(pstate, exprLocation(orig_expr))));
 	}
 
+	if (IsA(expr, Param))
+	{
+		if (pstate->p_param_assign_orig_hook)
+			pstate->p_param_assign_orig_hook(pstate, castNode(Param, expr), RelationGetRelid(pstate->p_target_relation), attrno);
+	}
+
 	pstate->p_expr_kind = sv_expr_kind;
 
 	return expr;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 1664fcee2a..1e0fde1d16 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 7bec4e4ff5..c707672fb8 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -71,6 +71,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"
 
@@ -665,6 +666,8 @@ pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 								 const char *query_string,
 								 Oid **paramTypes,
 								 int *numParams,
+								 Oid **paramOrigTbls,
+								 AttrNumber **paramOrigCols,
 								 QueryEnvironment *queryEnv)
 {
 	Query	   *query;
@@ -679,7 +682,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.
@@ -1363,6 +1366,8 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	bool		is_named;
 	bool		save_log_statement_stats = log_statement_stats;
 	char		msec_str[32];
+	Oid		   *paramOrigTbls = palloc(numParams * sizeof(Oid));
+	AttrNumber *paramOrigCols = palloc(numParams * sizeof(AttrNumber));
 
 	/*
 	 * Report query to various monitoring facilities.
@@ -1483,6 +1488,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 */
@@ -1513,6 +1520,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 */
@@ -1810,6 +1819,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;
@@ -2601,8 +2620,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 aa00815787..549d31e5d4 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 495e449a9e..405f08a9b3 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 a16a63f495..1e23ebb39a 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -24,7 +24,9 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_namespace.h"
@@ -2658,6 +2660,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 +3700,75 @@ 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_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 0d6a295674..f6893dd8b7 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -340,6 +340,8 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 				   MemoryContext querytree_context,
 				   Oid *param_types,
 				   int num_params,
+				   Oid *param_origtbls,
+				   AttrNumber *param_origcols,
 				   ParserSetupHook parserSetup,
 				   void *parserSetupArg,
 				   int cursor_options,
@@ -419,6 +421,14 @@ CompleteCachedPlan(CachedPlanSource *plansource,
 	{
 		plansource->param_types = (Oid *) palloc(num_params * sizeof(Oid));
 		memcpy(plansource->param_types, param_types, num_params * sizeof(Oid));
+
+		plansource->param_origtbls = (Oid *) palloc0(num_params * sizeof(Oid));
+		if (param_origtbls)
+			memcpy(plansource->param_origtbls, param_origtbls, num_params * sizeof(Oid));
+
+		plansource->param_origcols = (AttrNumber *) palloc0(num_params * sizeof(AttrNumber));
+		if (param_origcols)
+			memcpy(plansource->param_origcols, param_origcols, num_params * sizeof(AttrNumber));
 	}
 	else
 		plansource->param_types = NULL;
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index eec644ec84..6337f27bfc 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 0543c574c6..de837ec3cd 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 794e6e7ce9..5615b93603 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 fcc5f6bd05..47ba98eff3 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 233198afc0..8a2bd7fb95 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 28baa68fd4..960c3f39a9 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 d25709ad5f..5f162c698b 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 *oprinfo);
 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 69ee939d44..80bd29c2f3 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 5de3241eb4..e562a677ed 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 d665b257c9..078f90b607 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -91,6 +91,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;
@@ -152,6 +153,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},
@@ -422,6 +424,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)
@@ -647,6 +651,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 2873b662fb..ec01990727 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
@@ -3971,8 +4019,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 61188d96f2..ca64a51599 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 e611e3266d..d6846acf5f 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 327a69487b..ee4fc41ff6 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 f8ce1a0706..9f4fce26fd 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -195,6 +195,7 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\g [(OPTIONS)] [FILE]  execute query (and send result to file or |pipe);\n"
 		  "                         \\g with no arguments is equivalent to a semicolon\n");
 	HELP0("  \\gdesc                 describe result of query, without executing it\n");
+	HELP0("  \\gencr [PARAM]...      execute query, with support for parameter encryption\n");
 	HELP0("  \\gexec                 execute query, then execute each value in its result\n");
 	HELP0("  \\gset [PREFIX]         execute query and store result in psql variables\n");
 	HELP0("  \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n");
@@ -412,6 +413,8 @@ helpVariables(unsigned short int pager)
 		  "    true if last query failed, else false\n");
 	HELP0("  FETCH_COUNT\n"
 		  "    the number of result rows to fetch and display at a time (0 = unlimited)\n");
+	HELP0("  HIDE_COLUMN_ENCRYPTION\n"
+		  "    if set, column encryption details are not displayed\n");
 	HELP0("  HIDE_TABLEAM\n"
 		  "    if set, table access methods are not displayed\n");
 	HELP0("  HIDE_TOAST_COMPRESSION\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 2399cffa3f..636729df1d 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -95,6 +95,10 @@ typedef struct _psqlSettings
 
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
 	bool		gdesc_flag;		/* one-shot request to describe query result */
+	bool		gencr_flag;		/* one-shot request to send query with support
+								 * for parameter encryption */
+	int			num_params;		/* number of query parameters */
+	char	  **params;			/* query parameters */
 	bool		gexec_flag;		/* one-shot request to execute query result */
 	bool		crosstab_flag;	/* one-shot request to crosstab result */
 	char	   *ctv_args[4];	/* \crosstabview arguments */
@@ -134,6 +138,7 @@ typedef struct _psqlSettings
 	bool		quiet;
 	bool		singleline;
 	bool		singlestep;
+	bool		hide_column_encryption;
 	bool		hide_compression;
 	bool		hide_tableam;
 	int			fetch_count;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index e2e5678e2d..17ddd96e5e 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 62a39779b9..f994d5000b 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");
@@ -2785,6 +2813,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",
@@ -3509,6 +3557,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/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index 1d31b256fc..4812f862ca 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..fabad38ac0 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,7 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPrepared2           187
+PQsendQueryPrepared2      188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 917b19e0e9..a2acfc2a7c 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 0000000000..974a3aa810
--- /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 0000000000..b0093dc527
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * encryption support
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FE_ENCRYPT_H
+#define FE_ENCRYPT_H
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+extern unsigned char *decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg,
+											int fromlen, const unsigned char *from,
+											int *tolen);
+
+extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg,
+									const unsigned char *input, int inputlen,
+									const char **errmsgp);
+
+extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg,
+									const unsigned char *value, int *nbytesp, bool enc_det);
+
+#endif							/* FE_ENCRYPT_H */
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index bb874f7f50..cd9c0f91f8 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,381 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+int
+pqSaveColumnMasterKey(PGconn *conn, int keyid, const char *keyname,
+					  const char *keyrealm)
+{
+	char	   *keyname_copy;
+	char	   *keyrealm_copy;
+	bool		found;
+
+	keyname_copy = strdup(keyname);
+	if (!keyname_copy)
+		return EOF;
+	keyrealm_copy = strdup(keyrealm);
+	if (!keyrealm_copy)
+	{
+		free(keyname_copy);
+		return EOF;
+	}
+
+	found = false;
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		struct pg_cmk *checkcmk = &conn->cmks[i];
+
+		/* replace existing? */
+		if (checkcmk->cmkid == keyid)
+		{
+			free(checkcmk->cmkname);
+			free(checkcmk->cmkrealm);
+			checkcmk->cmkname = keyname_copy;
+			checkcmk->cmkrealm = keyrealm_copy;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newncmks;
+		struct pg_cmk *newcmks;
+		struct pg_cmk *newcmk;
+
+		newncmks = conn->ncmks + 1;
+		if (newncmks <= 0)
+			return EOF;
+		newcmks = realloc(conn->cmks, newncmks * sizeof(struct pg_cmk));
+		if (!newcmks)
+		{
+			free(keyname_copy);
+			free(keyrealm_copy);
+			return EOF;
+		}
+
+		newcmk = &newcmks[newncmks - 1];
+		newcmk->cmkid = keyid;
+		newcmk->cmkname = keyname_copy;
+		newcmk->cmkrealm = keyrealm_copy;
+
+		conn->ncmks = newncmks;
+		conn->cmks = newcmks;
+	}
+
+	return 0;
+}
+
+/*
+ * Replace placeholders in input string.  Return value malloc'ed.
+ */
+static char *
+replace_cmk_placeholders(const char *in, const char *cmkname, const char *cmkrealm, int cmkalg, const char *b64data)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s;
+
+						switch (cmkalg)
+						{
+							case PG_CMK_RSAES_OAEP_SHA_1:
+								s = "RSAES_OAEP_SHA_1";
+								break;
+							case PG_CMK_RSAES_OAEP_SHA_256:
+								s = "RSAES_OAEP_SHA_256";
+								break;
+							default:
+								s = "INVALID";
+						}
+						appendPQExpBufferStr(&buf, s);
+					}
+					p++;
+					break;
+				case 'b':
+					appendPQExpBufferStr(&buf, b64data);
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'r':
+					appendPQExpBufferStr(&buf, cmkrealm);
+					p++;
+					break;
+				default:
+					appendPQExpBufferChar(&buf, p[0]);
+			}
+		}
+		else
+			appendPQExpBufferChar(&buf, p[0]);
+	}
+
+	return buf.data;
+}
+
+#ifndef USE_SSL
+/*
+ * Dummy implementation for non-SSL builds
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	appendPQExpBuffer(&conn->errorMessage,
+					  libpq_gettext("column encryption not supported by this build\n"));
+	return NULL;
+}
+#endif
+
+/*
+ * Decrypt a CEK using the given CMK.  The ciphertext is passed in
+ * "from" and "fromlen".  Return the decrypted value in a malloc'ed area, its
+ * length via "tolen".  Return NULL on error; add error messages directly to
+ * "conn".
+ */
+static unsigned char *
+decrypt_cek(PGconn *conn, const PGCMK *cmk, int cmkalg,
+			int fromlen, const unsigned char *from,
+			int *tolen)
+{
+	char	   *cmklookup;
+	bool		found = false;
+	unsigned char *result = NULL;
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+		return NULL;
+	}
+
+	/*
+	 * Analyze semicolon-separated list
+	 */
+	for (char *s = strtok(cmklookup, ";"); s; s = strtok(NULL, ";"))
+	{
+		char	   *sep;
+
+		/* split found token at '=' */
+		sep = strchr(s, '=');
+		if (!sep)
+		{
+			appendPQExpBuffer(&conn->errorMessage,
+							  libpq_gettext("syntax error in CMK lookup specification, missing \"%c\": %s\n"), '=', s);
+			break;
+		}
+
+		/* matching realm? */
+		if (strncmp(s, "*", sep - s) == 0 || strncmp(s, cmk->cmkrealm, sep - s) == 0)
+		{
+			char	   *sep2;
+
+			found = true;
+
+			sep2 = strchr(sep, ':');
+			if (!sep2)
+			{
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("syntax error in CMK lookup specification, missing \"%c\": %s\n"), ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+				FILE	   *fp;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				fp = fopen(cmkfilename, "rb");
+				if (fp)
+				{
+					result = decrypt_cek_from_file(conn, fp, cmkalg, fromlen, from, tolen);
+					fclose(fp);
+				}
+				else
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not open file \"%s\": %m\n"), cmkfilename);
+					free(cmkfilename);
+					goto fail;
+				}
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				int			enclen;
+				char	   *enc;
+				char	   *command;
+				FILE	   *fp;
+				char		buf[4096];
+
+				enclen = pg_b64_enc_len(fromlen);
+				enc = malloc(enclen + 1);
+				if (!enc)
+				{
+					appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+					goto fail;
+				}
+				enclen = pg_b64_encode((const char *) from, fromlen, enc, enclen);
+				if (enclen < 0)
+				{
+					appendPQExpBuffer(&conn->errorMessage, libpq_gettext("base64 encoding failed\n"));
+					free(enc);
+					goto fail;
+				}
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, enc);
+				free(enc);
+				fp = popen(command, "r");
+				free(command);
+				if (!fp)
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not run command \"%s\": %m\n"), command);
+					goto fail;
+				}
+				if (fgets(buf, sizeof(buf), fp))
+				{
+					int			linelen;
+					int			declen;
+					char	   *dec;
+
+					linelen = strlen(buf);
+					if (buf[linelen - 1] == '\n')
+					{
+						buf[linelen - 1] = '\0';
+						linelen--;
+					}
+					declen = pg_b64_dec_len(linelen);
+					dec = malloc(declen);
+					if (!dec)
+					{
+						appendPQExpBuffer(&conn->errorMessage, libpq_gettext("out of memory\n"));
+						goto fail;
+					}
+					declen = pg_b64_decode(buf, linelen, dec, declen);
+					if (declen < 0)
+					{
+						appendPQExpBuffer(&conn->errorMessage,
+										  libpq_gettext("base64 decoding failed\n"));
+						free(dec);
+						goto fail;
+					}
+					result = (unsigned char *) dec;
+					*tolen = declen;
+				}
+				else
+				{
+					appendPQExpBuffer(&conn->errorMessage,
+									  libpq_gettext("could not read from command: %m\n"));
+					pclose(fp);
+					goto fail;
+				}
+				pclose(fp);
+			}
+			else
+			{
+				appendPQExpBuffer(&conn->errorMessage,
+								  libpq_gettext("CMK lookup scheme \"%s\" not recognized\n"), sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("no CMK lookup found for realm \"%s\"\n"), cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+int
+pqSaveColumnEncryptionKey(PGconn *conn, int keyid, int cmkid, int cmkalg, const unsigned char *value, int len)
+{
+	PGCMK	   *cmk = NULL;
+	unsigned char *plainval = NULL;
+	int			plainvallen = 0;
+	bool		found;
+
+	for (int i = 0; i < conn->ncmks; i++)
+	{
+		if (conn->cmks[i].cmkid == cmkid)
+		{
+			cmk = &conn->cmks[i];
+			break;
+		}
+	}
+
+	if (!cmk)
+		return EOF;
+
+	plainval = decrypt_cek(conn, cmk, cmkalg, len, value, &plainvallen);
+	if (!plainval)
+		return EOF;
+
+	found = false;
+	for (int i = 0; i < conn->nceks; i++)
+	{
+		struct pg_cek *checkcek = &conn->ceks[i];
+
+		/* replace existing? */
+		if (checkcek->cekid == keyid)
+		{
+			free(checkcek->cekdata);
+			checkcek->cekdata = plainval;
+			checkcek->cekdatalen = plainvallen;
+			found = true;
+			break;
+		}
+	}
+
+	/* append new? */
+	if (!found)
+	{
+		int			newnceks;
+		struct pg_cek *newceks;
+		struct pg_cek *newcek;
+
+		newnceks = conn->nceks + 1;
+		if (newnceks <= 0)
+		{
+			free(plainval);
+			return EOF;
+		}
+		newceks = realloc(conn->ceks, newnceks * sizeof(struct pg_cek));
+		if (!newceks)
+		{
+			free(plainval);
+			return EOF;
+		}
+
+		newcek = &newceks[newnceks - 1];
+		newcek->cekid = keyid;
+		newcek->cekdata = plainval;
+		newcek->cekdatalen = plainvallen;
+
+		conn->nceks = newnceks;
+		conn->ceks = newceks;
+	}
+
+	return 0;
+}
 
 /*
  * pqRowProcessor
@@ -1253,6 +1633,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 +1689,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;
@@ -1587,7 +2017,9 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   NULL,
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1705,6 +2137,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;
@@ -1732,7 +2178,9 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   paramForceColumnEncryptions,
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1833,7 +2281,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;
@@ -1880,14 +2330,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;
 		}
 	}
@@ -1906,6 +2395,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1924,9 +2414,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
 		{
@@ -2380,12 +2915,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);
 }
@@ -3683,6 +4233,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)
 {
@@ -3868,6 +4429,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 bbfb55542d..dbda7d9c7f 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);
@@ -317,6 +319,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 &&
@@ -574,6 +592,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) ||
@@ -588,6 +608,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.
@@ -609,8 +644,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;
 	}
 
@@ -712,10 +749,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! */
@@ -1413,6 +1471,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.
@@ -1441,6 +1533,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.
@@ -2266,6 +2441,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 5d68cf2eb3..d2ae0e03bb 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);
+		}
 	}
 }
 
@@ -647,10 +662,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 +682,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			fprintf(conn->Pfdebug, "Terminate");
 			/* No message content */
 			break;
+		case 'y':
+			fprintf(conn->Pfdebug, "ColumnMasterKey\tTODO");
+			break;
+		case 'Y':
+			fprintf(conn->Pfdebug, "ColumnEncryptionKey\tTODO");
+			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 7986445f1a..42fbe0cd49 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -267,6 +267,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -438,6 +440,15 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPrepared2(PGconn *conn,
+								 const char *stmtName,
+								 int nParams,
+								 const char *const *paramValues,
+								 const int *paramLengths,
+								 const int *paramFormats,
+								 const int *paramForceColumnEncryptions,
+								 int resultFormat,
+								 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +472,15 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPrepared2(PGconn *conn,
+								 const char *stmtName,
+								 int nParams,
+								 const char *const *paramValues,
+								 const int *paramLengths,
+								 const int *paramFormats,
+								 const int *paramForceColumnEncryptions,
+								 int resultFormat,
+								 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +551,7 @@ extern int	PQfformat(const PGresult *res, int field_num);
 extern Oid	PQftype(const PGresult *res, int field_num);
 extern int	PQfsize(const PGresult *res, int field_num);
 extern int	PQfmod(const PGresult *res, int field_num);
+extern int	PQfisencrypted(const PGresult *res, int field_num);
 extern char *PQcmdStatus(PGresult *res);
 extern char *PQoidStatus(const PGresult *res);	/* old and ugly */
 extern Oid	PQoidValue(const PGresult *res);	/* new and improved */
@@ -540,6 +561,7 @@ extern int	PQgetlength(const PGresult *res, int tup_num, int field_num);
 extern int	PQgetisnull(const PGresult *res, int tup_num, int field_num);
 extern int	PQnparams(const PGresult *res);
 extern Oid	PQparamtype(const PGresult *res, int param_num);
+extern int	PQparamisencrypted(const PGresult *res, int param_num);
 
 /* Describe prepared statements and portals */
 extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index c75ed63a2c..09307dab11 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/nls.mk b/src/interfaces/libpq/nls.mk
index 9256b426c1..c45fe86d1d 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 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 7bf35602b0..19f3f19dcd 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9627,7 +9627,7 @@ DO $d$
     END;
 $d$;
 ERROR:  invalid option "password"
-HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
+HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, cmklookup, column_encryption, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
 CONTEXT:  SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
 PL/pgSQL function inline_code_block line 3 at EXECUTE
 -- If we add a password for our user mapping instead, we should get a different

base-commit: c98b6acdb252546e9bea0b9a37d95ca63d2ff0fa
-- 
2.37.1

#32Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Masahiko Sawada (#23)
Re: Transparent column encryption

On 20.07.22 08:12, Masahiko Sawada wrote:

---
Regarding the documentation, I'd like to have a page that describes
the generic information of the transparent column encryption for users
such as what this feature actually does, what can be achieved by this
feature, CMK rotation, and its known limitations. The patch has
"Transparent Column Encryption" section in protocol.sgml but it seems
to be more internal information.

I have added more documentation in the v6 patch.

---
In datatype.sgml, it says "Thus, clients that don't support
transparent column encryption or have disabled it will see the
encrypted values as byte arrays." but I got an error rather than
encrypted values when I tried to connect to the server using by
clients that don't support the encryption:

postgres(1:6040)=# select * from tbl;
no CMK lookup found for realm ""

This has now been improved in v6. The protocol changes need to be
activated explicitly at connection time, so if you use a client that
doesn't support it or activates it, you get the described behavior.

---
In single-user mode, the user cannot decrypt the encrypted value but
probably it's fine in practice.

Yes, there is nothing really to do about that.

---
Regarding the column master key rotation, would it be useful if we
provide a tool for that? For example, it takes old and new CMK as
input, re-encrypt all CEKs realted to the CMK, and registers them to
the server.

I imagine users using a variety of key management systems, so I don't
see how a single tool would work. But it's something we can think about
in the future.

---
Is there any convenient way to load a large amount of test data to the
encrypted columns? I tried to use generate_series() but it seems not
to work as it generates the data on the server side:

No, that doesn't work, by design. You'd have to write a client program
to generate the data.

I've also tried to load the data from a file on the client by using
\copy command, but it seems not to work:

postgres(1:80556)=# copy (select generate_series(1, 1000)::text) to
'/tmp/tmp.dat';
COPY 1000
postgres(1:80556)=# \copy a from '/tmp/tmp.dat'
COPY 1000
postgres(1:80556)=# select * from a;
out out memory

This was a bug that I have fixed.

---
I got SEGV in the following two situations:

I have fixed these.

#33Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#30)
Re: Transparent column encryption

On 27.07.22 01:19, Jacob Champion wrote:

Now, if we don't have a padding system
built into the feature, then that does put even more on the user; it's
hard to argue with that.

Right. If they can even fix it at all. Having a well-documented padding
feature would not only help mitigate that, it would conveniently hang a
big sign on the caveats that exist.

I would be interested in learning more about such padding systems. I
have done a lot of reading for this development project, and I have
never come across a cryptographic approach to hide length differences by
padding. Of course, padding to the block cipher's block size is already
part of the process, but that is done out of necessity, not because you
want to disguise the length. Are there any other methods? I'm
interested to learn more.

#34Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#33)
Re: Transparent column encryption

On Tue, Aug 30, 2022 at 4:53 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

I would be interested in learning more about such padding systems. I
have done a lot of reading for this development project, and I have
never come across a cryptographic approach to hide length differences by
padding. Of course, padding to the block cipher's block size is already
part of the process, but that is done out of necessity, not because you
want to disguise the length. Are there any other methods? I'm
interested to learn more.

TLS 1.3 has one example. Here is a description from GnuTLS:
https://gnutls.org/manual/html_node/On-Record-Padding.html (Note the
option to turn on constant-time padding; that may not be a good
tradeoff for us if we're focusing on offline attacks.)

Here's a recent paper that claims to formally characterize length
hiding, but it's behind a wall and I haven't read it:
https://dl.acm.org/doi/abs/10.1145/3460120.3484590

I'll try to find more when I get the chance.

--Jacob

#35Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#31)
1 attachment(s)
Re: Transparent column encryption

Here is an updated patch that resolves some merge conflicts; no
functionality changes over v6.

Show quoted text

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.)

Attachments:

v7-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v7-0001-Transparent-column-encryption.patchDownload
From 262b96318f36b708f042f02b431e66048918813b Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Tue, 13 Sep 2022 10:24:57 +0200
Subject: [PATCH v7] 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
---
 .../postgres_fdw/expected/postgres_fdw.out    |   2 +-
 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                  |  62 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |   3 +
 src/backend/catalog/objectaddress.c           | 183 +++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  15 +
 src/backend/commands/colenccmds.c             | 361 +++++++++
 src/backend/commands/event_trigger.c          |   9 +
 src/backend/commands/prepare.c                |  74 +-
 src/backend/commands/seclabel.c               |   2 +
 src/backend/commands/tablecmds.c              |  83 ++
 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           |  93 +++
 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/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                |  16 +
 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                 |   5 +
 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               |  31 +-
 src/interfaces/libpq/libpq-fe.h               |  22 +
 src/interfaces/libpq/libpq-int.h              |  37 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/test/Makefile                             |   3 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  29 +
 .../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 +
 .../regress/expected/column_encryption.out    | 115 +++
 src/test/regress/expected/object_address.out  |  47 +-
 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    | 107 +++
 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 +
 130 files changed, 7414 insertions(+), 156 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/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/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 7bf35602b0..19f3f19dcd 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -9627,7 +9627,7 @@ DO $d$
     END;
 $d$;
 ERROR:  invalid option "password"
-HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
+HINT:  Valid options in this context are: service, passfile, channel_binding, connect_timeout, dbname, host, hostaddr, port, options, application_name, keepalives, keepalives_idle, keepalives_interval, keepalives_count, tcp_user_timeout, sslmode, sslcompression, sslcert, sslkey, sslrootcert, sslcrl, sslcrldir, sslsni, requirepeer, ssl_min_protocol_version, ssl_max_protocol_version, gssencmode, krbsrvname, gsslib, target_session_attrs, cmklookup, column_encryption, use_remote_estimate, fdw_startup_cost, fdw_tuple_cost, extensions, updatable, truncatable, fetch_size, batch_size, async_capable, parallel_commit, keep_connections
 CONTEXT:  SQL statement "ALTER SERVER loopback_nopw OPTIONS (ADD password 'dummypw')"
 PL/pgSQL function inline_code_block line 3 at EXECUTE
 -- If we add a password for our user mapping instead, we should get a different
diff --git a/doc/src/sgml/acronyms.sgml b/doc/src/sgml/acronyms.sgml
index 2df6559acc..bd1a0185ed 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 00f833d210..638be1c850 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 0258b192e0..05286dd3e5 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 03c0193709..8cf69fe10f 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 d6d0a3a814..10507e4391 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 8a1a9e9932..7968630aa0 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.
@@ -4686,6 +4903,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>
 
@@ -4736,6 +4993,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"/>
@@ -7766,6 +8024,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 87870c5b10..f421ca0863 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>
 
@@ -4049,6 +4111,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>
@@ -5145,6 +5341,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>
 
@@ -5533,6 +5769,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>
 
@@ -7336,6 +7601,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 e90a0e1f83..331b1f010b 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 0000000000..7597cd80ca
--- /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 0000000000..13e310b227
--- /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 0000000000..a94b0924b1
--- /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 0000000000..ec5fa4cde5
--- /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 c14b2010d8..feebf51fd9 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 0000000000..9d157de9c5
--- /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 0000000000..c85098ea1c
--- /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 c08276bc0a..83aab1cf0e 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 5d54074e01..bdde5fc8c1 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 9494f28063..cd61e99e86 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 a3b743e8c1..e1425c222f 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 c99ae54cb0..86482eb4c8 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 d2f3b57288..80ab5e875b 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 d6fb261e20..e3ecc64ea7 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attrealtypid != attr2->attrealtypid)
+			return false;
+		if (attr1->attencalg != attr2->attencalg)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 10bf26ce7c..cea91d4a88 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 89a0221ec9..7e1a91b974 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 17ff617fba..4a3a62bfd8 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"
@@ -3579,6 +3581,8 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AMPROC:
 					case OBJECT_ATTRIBUTE:
 					case OBJECT_CAST:
+					case OBJECT_CEK:
+					case OBJECT_CMK:
 					case OBJECT_DEFAULT:
 					case OBJECT_DEFACL:
 					case OBJECT_DOMCONSTRAINT:
@@ -3609,6 +3613,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;
@@ -5582,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 39768fa22b..6f1ea268f0 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 9b03579e6e..e91b0b806d 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -746,6 +746,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attrealtypid - 1] = ObjectIdGetDatum(attrs->attrealtypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attencalg - 1] = Int16GetDatum(attrs->attencalg);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 798c1a2d1e..7d9801d645 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", -1
+	},
+	/* 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:
@@ -1294,6 +1353,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);
@@ -2322,6 +2391,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:
@@ -2629,6 +2700,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);
@@ -3089,6 +3170,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;
@@ -4513,6 +4636,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);
@@ -4978,6 +5113,54 @@ 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:
+			elog(ERROR, "TODO");
+			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 48f7348f91..69f6175c60 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 55219bb097..6caa21e325 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 0000000000..c35125f65f
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,361 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 = GetSysCacheOid2(CEKDATACEKCMK, Anum_pg_colenckeydata_oid,
+									 ObjectIdGetDatum(cekoid),
+									 ObjectIdGetDatum(cmkoid));
+		if (!OidIsValid(cekdataoid))
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("column encryption key \"%s\" has no data for master key \"%s\"",
+							get_cek_name(cekoid, false), get_cmk_name(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);
+		// TODO: check for duplicates
+		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 635d05405e..f008eae6f8 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -951,6 +951,8 @@ EventTriggerSupportsObjectType(ObjectType obtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLUMN:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
@@ -1027,6 +1029,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 +2061,8 @@ stringify_grant_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
@@ -2139,6 +2146,8 @@ stringify_adefprivs_objtype(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_DEFAULT:
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index c4b54d0547..65dde32441 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 7ae19b98bb..9ccdf7616c 100644
--- a/src/backend/commands/seclabel.c
+++ b/src/backend/commands/seclabel.c
@@ -66,6 +66,8 @@ SecLabelSupportsObjectType(ObjectType objtype)
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
 		case OBJECT_CAST:
+		case OBJECT_CEK:
+		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 e3233a8f38..14e7a7e6bd 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"
@@ -935,6 +936,82 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			ListCell   *lc;
+			char	   *cek = NULL;
+			Oid			cekoid;
+			bool		encdet = false;
+			int			alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+			foreach(lc, colDef->encryption)
+			{
+				DefElem    *el = lfirst_node(DefElem, lc);
+
+				if (strcmp(el->defname, "column_encryption_key") == 0)
+					cek = strVal(linitial(castNode(TypeName, el->arg)->names));
+				else if (strcmp(el->defname, "encryption_type") == 0)
+				{
+					char	   *val = strVal(linitial(castNode(TypeName, el->arg)->names));
+
+					if (strcmp(val, "deterministic") == 0)
+						encdet = true;
+					else if (strcmp(val, "randomized") == 0)
+						encdet = false;
+					else
+						ereport(ERROR,
+								errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								errmsg("unrecognized encryption type: %s", val));
+				}
+				else if (strcmp(el->defname, "algorithm") == 0)
+				{
+					char	   *val = strVal(el->arg);
+
+					if (strcmp(val, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+						alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+					else if (strcmp(val, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+						alg = PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+					else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+						alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+					else if (strcmp(val, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+						alg = PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+					else
+						ereport(ERROR,
+								errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								errmsg("unrecognized encryption algorithm: %s", val));
+				}
+				else
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("unrecognized column encryption parameter: %s", el->defname));
+			}
+
+			if (!cek)
+				ereport(ERROR,
+						errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+						errmsg("column encryption key must be specified"));
+
+			cekoid = GetSysCacheOid1(CEKNAME, Anum_pg_colenckey_oid, PointerGetDatum(cek));
+			if (!cekoid)
+				ereport(ERROR,
+						errcode(ERRCODE_UNDEFINED_OBJECT),
+						errmsg("column encryption key \"%s\" does not exist", cek));
+
+			attr->attcek = cekoid;
+			attr->attrealtypid = attr->atttypid;
+			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;
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -6868,6 +6945,9 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attgenerated = colDef->generated;
 	attribute.attisdropped = false;
 	attribute.attislocal = colDef->is_local;
+	attribute.attcek = 0; // TODO
+	attribute.attrealtypid = 0; // TODO
+	attribute.attencalg = 0; // TODO
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
 
@@ -12664,6 +12744,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:
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index e5ddcda0b4..74e46cc73e 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -24,6 +24,7 @@
 #include "access/xlog.h"
 #include "catalog/pg_authid.h"
 #include "commands/variable.h"
+#include "libpq/libpq-be.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "utils/acl.h"
@@ -648,7 +649,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 29bc26669b..cde5b0e087 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2279,6 +2279,8 @@ _SPI_prepare_plan(const char *src, SPIPlanPtr plan)
 						   NULL,
 						   plan->argtypes,
 						   plan->nargs,
+						   NULL,
+						   NULL,
 						   plan->parserSetup,
 						   plan->parserSetupArg,
 						   plan->cursor_options,
@@ -2516,6 +2518,8 @@ _SPI_execute_plan(SPIPlanPtr plan, const SPIExecuteOptions *options,
 							   NULL,
 							   plan->argtypes,
 							   plan->nargs,
+							   NULL,
+							   NULL,
 							   plan->parserSetup,
 							   plan->parserSetupArg,
 							   plan->cursor_options,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 3bac350bf5..c7285841f3 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4013,6 +4013,8 @@ raw_expression_tree_walker(Node *node,
 					return true;
 				if (walker(coldef->compression, context))
 					return true;
+				if (walker(coldef->encryption, context))
+					return true;
 				if (walker(coldef->raw_default, context))
 					return true;
 				if (walker(coldef->collClause, context))
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 6688c2a865..ce9b7a5f5f 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 ea33784316..b5d4ce6d0b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -281,7 +281,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
@@ -421,6 +421,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
@@ -594,6 +595,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
@@ -692,8 +694,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
@@ -716,7 +718,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 f668abfcb3..d0bec0a06d 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 4e1593d900..e1e1381369 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -595,6 +595,12 @@ transformAssignedExpr(ParseState *pstate,
 					 parser_errposition(pstate, exprLocation(orig_expr))));
 	}
 
+	if (IsA(expr, Param))
+	{
+		if (pstate->p_param_assign_orig_hook)
+			pstate->p_param_assign_orig_hook(pstate, castNode(Param, expr), RelationGetRelid(pstate->p_target_relation), attrno);
+	}
+
 	pstate->p_expr_kind = sv_expr_kind;
 
 	return expr;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index e75611fdd5..9690ae395d 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 c6ca3b5b3d..c40aa3a697 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -71,6 +71,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"
 
@@ -665,6 +666,8 @@ pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 								 const char *query_string,
 								 Oid **paramTypes,
 								 int *numParams,
+								 Oid **paramOrigTbls,
+								 AttrNumber **paramOrigCols,
 								 QueryEnvironment *queryEnv)
 {
 	Query	   *query;
@@ -679,7 +682,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.
@@ -1363,6 +1366,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.
@@ -1483,6 +1488,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 */
@@ -1513,6 +1520,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 */
@@ -1810,6 +1819,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;
@@ -2600,8 +2619,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 aa00815787..549d31e5d4 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 495e449a9e..405f08a9b3 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 a16a63f495..1e23ebb39a 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -24,7 +24,9 @@
 #include "catalog/pg_amop.h"
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_cast.h"
+#include "catalog/pg_colenckey.h"
 #include "catalog/pg_collation.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_namespace.h"
@@ -2658,6 +2660,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 +3700,75 @@ 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_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 0d6a295674..f2a5e029e3 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 eec644ec84..6337f27bfc 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 0543c574c6..de837ec3cd 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 395f817fa8..ea4ef62be7 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 fcc5f6bd05..47ba98eff3 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 233198afc0..8a2bd7fb95 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 28baa68fd4..960c3f39a9 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 67b6d9079e..da07852344 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 *oprinfo);
 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 69ee939d44..80bd29c2f3 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 5de3241eb4..e562a677ed 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 69ae027bd3..3ee7525e96 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -91,6 +91,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;
@@ -152,6 +153,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},
@@ -422,6 +424,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)
@@ -647,6 +651,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 2873b662fb..ec01990727 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
@@ -3971,8 +4019,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 a141146e70..2306f4ca60 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 e611e3266d..d6846acf5f 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 c645d66418..3fbfe5dc68 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 f8ce1a0706..9f4fce26fd 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -195,6 +195,7 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\g [(OPTIONS)] [FILE]  execute query (and send result to file or |pipe);\n"
 		  "                         \\g with no arguments is equivalent to a semicolon\n");
 	HELP0("  \\gdesc                 describe result of query, without executing it\n");
+	HELP0("  \\gencr [PARAM]...      execute query, with support for parameter encryption\n");
 	HELP0("  \\gexec                 execute query, then execute each value in its result\n");
 	HELP0("  \\gset [PREFIX]         execute query and store result in psql variables\n");
 	HELP0("  \\gx [(OPTIONS)] [FILE] as \\g, but forces expanded output mode\n");
@@ -412,6 +413,8 @@ helpVariables(unsigned short int pager)
 		  "    true if last query failed, else false\n");
 	HELP0("  FETCH_COUNT\n"
 		  "    the number of result rows to fetch and display at a time (0 = unlimited)\n");
+	HELP0("  HIDE_COLUMN_ENCRYPTION\n"
+		  "    if set, column encryption details are not displayed\n");
 	HELP0("  HIDE_TABLEAM\n"
 		  "    if set, table access methods are not displayed\n");
 	HELP0("  HIDE_TOAST_COMPRESSION\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 2399cffa3f..636729df1d 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -95,6 +95,10 @@ typedef struct _psqlSettings
 
 	char	   *gset_prefix;	/* one-shot prefix argument for \gset */
 	bool		gdesc_flag;		/* one-shot request to describe query result */
+	bool		gencr_flag;		/* one-shot request to send query with support
+								 * for parameter encryption */
+	int			num_params;		/* number of query parameters */
+	char	  **params;			/* query parameters */
 	bool		gexec_flag;		/* one-shot request to execute query result */
 	bool		crosstab_flag;	/* one-shot request to crosstab result */
 	char	   *ctv_args[4];	/* \crosstabview arguments */
@@ -134,6 +138,7 @@ typedef struct _psqlSettings
 	bool		quiet;
 	bool		singleline;
 	bool		singlestep;
+	bool		hide_column_encryption;
 	bool		hide_compression;
 	bool		hide_tableam;
 	int			fetch_count;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index f5b9e268f2..0b812f3322 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 f3465adb85..1d2f80c572 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",
@@ -3518,6 +3566,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 971a74cf22..db1f3b8811 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,6 +20,8 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 729c4c46c0..e237155971 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/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 61cd591430..a0bd708132 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4cc129bebd..dddb27113f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 053294c99f..550a53649b 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,15 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	bool		attislocal BKI_DEFAULT(t);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_colenckey);
+
+	/* real type if encrypted */
+	Oid			attrealtypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+
+	/* encryption algorithm (PG_CEK_* values) */
+	int16		attencalg BKI_DEFAULT(0);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index 4471eb6bbe..878b0bcbbf 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -546,4 +546,10 @@
 { castsource => 'tstzrange', casttarget => 'tstzmultirange',
   castfunc => 'tstzmultirange(tstzrange)', castcontext => 'e',
   castmethod => 'f' },
+
+{ castsource => 'bytea', casttarget => 'pg_encrypted_det', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+{ castsource => 'bytea', casttarget => 'pg_encrypted_rnd', castfunc => '0',
+  castcontext => 'e', castmethod => 'b' },
+
 ]
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..095ed712b0
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,55 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEY_H
+#define PG_COLENCKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckey_d.h"
+
+/* ----------------
+ *		pg_colenckey definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckey
+ * ----------------
+ */
+CATALOG(pg_colenckey,8234,ColumnEncKeyRelationId)
+{
+	Oid			oid;
+	NameData	cekname;
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_index, 8242, ColumnEncKeyNameIndexId, on pg_colenckey using btree(cekname name_ops));
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  For clarity, the assigned numbers are not reused between CMKs
+ * and CEKs, but that is not technically required.  In either case, don't
+ * assign zero, so that that can be used as an invalid value.
+ */
+
+#define PG_CMK_RSAES_OAEP_SHA_1			1
+#define PG_CMK_RSAES_OAEP_SHA_256		2
+
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	130
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	131
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	132
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	133
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..3e4dea7218
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colenkeydata.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLENCKEYDATA_H
+#define PG_COLENCKEYDATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colenckeydata_d.h"
+
+/* ----------------
+ *		pg_colenckeydata definition. cpp turns this into
+ *		typedef struct FormData_pg_colenckeydata
+ * ----------------
+ */
+CATALOG(pg_colenckeydata,8250,ColumnEncKeyDataRelationId)
+{
+	Oid			oid;
+	Oid			ckdcekid BKI_LOOKUP(pg_colenckey);
+	Oid			ckdcmkid BKI_LOOKUP(pg_colmasterkey);
+	int16		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..0344cc4201
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,45 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_index, 8241, ColumnMasterKeyNameIndexId, on pg_colmasterkey using btree(cmkname name_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index dbcae7ffdd..0ca401ffe4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index bc5f8213f3..4737a7f9ed 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index b3b6a7e616..5343580b06 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index a07e737a33..7f6298452f 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 df45879463..851063cf5f 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 48a2559137..c1d30903b5 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 0000000000..d9741e100b
--- /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 6d452ec6d9..e32c7779c9 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 6958306a7d..3c256c39d5 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,8 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2056,6 +2059,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 dc379547c7..c310e124f1 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -28,7 +28,9 @@ extern PGDLLIMPORT post_parse_analyze_hook_type post_parse_analyze_hook;
 extern Query *parse_analyze_fixedparams(RawStmt *parseTree, const char *sourceText,
 										const Oid *paramTypes, int numParams, QueryEnvironment *queryEnv);
 extern Query *parse_analyze_varparams(RawStmt *parseTree, const char *sourceText,
-									  Oid **paramTypes, int *numParams, QueryEnvironment *queryEnv);
+									  Oid **paramTypes, int *numParams,
+									  Oid **paramOrigTbls, AttrNumber **paramOrigCols,
+									  QueryEnvironment *queryEnv);
 extern Query *parse_analyze_withcb(RawStmt *parseTree, const char *sourceText,
 								   ParserSetupHook parserSetup,
 								   void *parserSetupArg,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 9a7cc0c6bd..813cf3d23d 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 962ebf65de..f6a7178766 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -93,6 +93,7 @@ typedef Node *(*ParseParamRefHook) (ParseState *pstate, ParamRef *pref);
 typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param,
 								  Oid targetTypeId, int32 targetTypeMod,
 								  int location);
+typedef void (*ParamAssignOrigHook) (ParseState *pstate, Param *param, Oid origtbl, AttrNumber origcol);
 
 
 /*
@@ -222,6 +223,7 @@ struct ParseState
 	PostParseColumnRefHook p_post_columnref_hook;
 	ParseParamRefHook p_paramref_hook;
 	CoerceParamHook p_coerce_param_hook;
+	ParamAssignOrigHook p_param_assign_orig_hook;
 	void	   *p_ref_hook_state;	/* common passthrough link for above */
 };
 
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index df1ee660d8..da202b28a7 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -18,7 +18,8 @@
 extern void setup_parse_fixed_parameters(ParseState *pstate,
 										 const Oid *paramTypes, int numParams);
 extern void setup_parse_variable_parameters(ParseState *pstate,
-											Oid **paramTypes, int *numParams);
+											Oid **paramTypes, int *numParams,
+											Oid **paramOrigTbls, AttrNumber **paramOrigCols);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f..d82a2e1171 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 70d9dab25b..3038ceb522 100644
--- a/src/include/tcop/tcopprot.h
+++ b/src/include/tcop/tcopprot.h
@@ -53,6 +53,8 @@ extern List *pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 											  const char *query_string,
 											  Oid **paramTypes,
 											  int *numParams,
+											  Oid **paramOrigTbls,
+											  AttrNumber **paramOrigCols,
 											  QueryEnvironment *queryEnv);
 extern List *pg_analyze_and_rewrite_withcb(RawStmt *parsetree,
 										   const char *query_string,
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 3d6411197c..bdb6d741d5 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 50f0288305..064b9cd338 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,10 @@ 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_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 0499635f59..9a7cf794ce 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 4463ea66be..489c6ee734 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 1d31b256fc..4812f862ca 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..fabad38ac0 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -186,3 +186,7 @@ PQpipelineStatus          183
 PQsetTraceFlags           184
 PQmblenBounded            185
 PQsendFlushRequest        186
+PQexecPrepared2           187
+PQsendQueryPrepared2      188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 917b19e0e9..a2acfc2a7c 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 0000000000..974a3aa810
--- /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 0000000000..b0093dc527
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * encryption support
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/interfaces/libpq/fe-encrypt.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FE_ENCRYPT_H
+#define FE_ENCRYPT_H
+
+#include "libpq-fe.h"
+#include "libpq-int.h"
+
+extern unsigned char *decrypt_cek_from_file(PGconn *conn, FILE *fp, int cmkalg,
+											int fromlen, const unsigned char *from,
+											int *tolen);
+
+extern unsigned char *decrypt_value(PGresult *res, const PGCEK *cek, int cekalg,
+									const unsigned char *input, int inputlen,
+									const char **errmsgp);
+
+extern unsigned char *encrypt_value(PGconn *conn, const PGCEK *cek, int cekalg,
+									const unsigned char *value, int *nbytesp, bool enc_det);
+
+#endif							/* FE_ENCRYPT_H */
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index bb874f7f50..4b50b285e0 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;
@@ -1587,7 +2018,9 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   NULL,
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1705,6 +2138,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;
@@ -1732,7 +2179,9 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   paramForceColumnEncryptions,
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1833,7 +2282,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;
@@ -1880,14 +2331,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;
 		}
 	}
@@ -1906,6 +2396,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1924,9 +2415,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
 		{
@@ -2380,12 +2916,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);
 }
@@ -3683,6 +4234,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)
 {
@@ -3868,6 +4430,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 bbfb55542d..dbda7d9c7f 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);
@@ -317,6 +319,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 &&
@@ -574,6 +592,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) ||
@@ -588,6 +608,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.
@@ -609,8 +644,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;
 	}
 
@@ -712,10 +749,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! */
@@ -1413,6 +1471,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.
@@ -1441,6 +1533,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.
@@ -2266,6 +2441,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 5d68cf2eb3..d2ae0e03bb 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);
+		}
 	}
 }
 
@@ -647,10 +662,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 +682,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			fprintf(conn->Pfdebug, "Terminate");
 			/* No message content */
 			break;
+		case 'y':
+			fprintf(conn->Pfdebug, "ColumnMasterKey\tTODO");
+			break;
+		case 'Y':
+			fprintf(conn->Pfdebug, "ColumnEncryptionKey\tTODO");
+			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 7986445f1a..42fbe0cd49 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -267,6 +267,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -438,6 +440,15 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPrepared2(PGconn *conn,
+								 const char *stmtName,
+								 int nParams,
+								 const char *const *paramValues,
+								 const int *paramLengths,
+								 const int *paramFormats,
+								 const int *paramForceColumnEncryptions,
+								 int resultFormat,
+								 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +472,15 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPrepared2(PGconn *conn,
+								 const char *stmtName,
+								 int nParams,
+								 const char *const *paramValues,
+								 const int *paramLengths,
+								 const int *paramFormats,
+								 const int *paramForceColumnEncryptions,
+								 int resultFormat,
+								 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +551,7 @@ extern int	PQfformat(const PGresult *res, int field_num);
 extern Oid	PQftype(const PGresult *res, int field_num);
 extern int	PQfsize(const PGresult *res, int field_num);
 extern int	PQfmod(const PGresult *res, int field_num);
+extern int	PQfisencrypted(const PGresult *res, int field_num);
 extern char *PQcmdStatus(PGresult *res);
 extern char *PQoidStatus(const PGresult *res);	/* old and ugly */
 extern Oid	PQoidValue(const PGresult *res);	/* new and improved */
@@ -540,6 +561,7 @@ extern int	PQgetlength(const PGresult *res, int tup_num, int field_num);
 extern int	PQgetisnull(const PGresult *res, int tup_num, int field_num);
 extern int	PQnparams(const PGresult *res);
 extern Oid	PQparamtype(const PGresult *res, int param_num);
+extern int	PQparamisencrypted(const PGresult *res, int param_num);
 
 /* Describe prepared statements and portals */
 extern PGresult *PQdescribePrepared(PGconn *conn, const char *stmt);
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index c75ed63a2c..09307dab11 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/nls.mk b/src/interfaces/libpq/nls.mk
index 9256b426c1..c45fe86d1d 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 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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/test/Makefile b/src/test/Makefile
index 69ef074d75..2300be8332 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -32,6 +32,7 @@ SUBDIRS += ldap
 endif
 endif
 ifeq ($(with_ssl),openssl)
+SUBDIRS += column_encryption
 ifneq (,$(filter ssl,$(PG_TEST_EXTRA)))
 SUBDIRS += ssl
 endif
@@ -41,7 +42,7 @@ endif
 # clean" etc to recurse into them.  (We must filter out those that we
 # have conditionally included into SUBDIRS above, else there will be
 # make confusion.)
-ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl)
+ALWAYS_SUBDIRS = $(filter-out $(SUBDIRS),examples kerberos icu ldap ssl column_encryption)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..7aca84b17a
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,29 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/column_encryption/Makefile
+#
+#-------------------------------------------------------------------------
+
+subdir = src/test/column_encryption
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+
+override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
+
+all: test_client
+
+check: all
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean maintainer-clean:
+	rm -f test_client.o test_client
+	rm -rf tmp_check
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..4a23414ee0
--- /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 0000000000..ec447872ab
--- /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 0000000000..636de88c78
--- /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 0000000000..9664349f14
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,41 @@
+#!/usr/bin/perl
+
+# Test/sample command for libpq cmklookup run scheme
+#
+# This just places the data into temporary files and runs the openssl
+# command on it.  (In practice, this could more simply be written as a
+# shell script, but this way it's more portable.)
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use MIME::Base64;
+
+my ($tmpdir, $cmkname, $alg, $b64data) = @ARGV;
+
+die unless $alg eq 'RSAES_OAEP_SHA_1';
+
+open my $fh, '>:raw', "${tmpdir}/input.tmp" or die;
+print $fh decode_base64($b64data);
+close $fh;
+
+system('openssl', 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', "${tmpdir}/input.tmp", '-out', "${tmpdir}/output.tmp") == 0 or die;
+
+open $fh, '<:raw', "${tmpdir}/output.tmp" or die;
+my $data = '';
+
+while (1) {
+	my $success = read $fh, $data, 100, length($data);
+	die $! if not defined $success;
+	last if not $success;
+}
+
+close $fh;
+
+unlink "${tmpdir}/input.tmp", "${tmpdir}/output.tmp";
+
+print encode_base64($data);
diff --git a/src/test/regress/expected/column_encryption.out b/src/test/regress/expected/column_encryption.out
new file mode 100644
index 0000000000..a31e5391cd
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,115 @@
+\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'
+);
+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       |              | 
+
+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 4117fc27c9..b43e24a9a1 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 relation')
+		('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
@@ -319,6 +322,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},{}: unsupported object type "column encryption key data"
+WARNING:  error for column encryption key data,{eins},{integer}: unsupported object type "column encryption key data"
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{}: unsupported object type "column encryption key data"
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: unsupported object type "column encryption key data"
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: unsupported object type "column encryption key data"
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: unsupported object type "column encryption key data"
+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
@@ -374,6 +395,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[]),
@@ -396,6 +425,9 @@ WITH objects (type, name, args) AS (VALUES
 				('type', '{addr_nsp.genenum}', '{}'),
 				('cast', '{int8}', '{int4}'),
 				('collation', '{default}', '{}'),
+				('column encryption key', '{addr_cek}', '{}'),
+--TODO:				('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}', '{}'),
@@ -493,7 +525,9 @@ SELECT (pg_identify_object(addr1.classid, addr1.objid, addr1.objsubid)).*,
  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)
+ column master key         |            | addr_cmk          | addr_cmk                                                             | t
+ column encryption key     |            | addr_cek          | addr_cek                                                             | t
+(52 rows)
 
 ---
 --- Cleanup resources
@@ -507,6 +541,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
@@ -542,6 +578,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
+--TODO:    ('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
@@ -572,6 +611,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
@@ -623,5 +663,8 @@ 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
 -- restore normal output mode
 \a\t
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 215eb899be..2aa0e16323 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attrealtypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,7 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 330eb0f765..646507f029 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 5815e17b39..4482a65d24 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 a7f5700edc..2ee5f7546d 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 9dd137415e..7ebf6fdb25 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 d3ac08c9ee..357095e1b9 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 9f644a0c1b..e68bccfdc8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -129,6 +129,9 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # ----------
 test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats
 
+# WIP
+test: column_encryption
+
 # event_trigger cannot run concurrently with any test that runs DDL
 # oidjoins is read-only, though, and should run late for best coverage
 test: event_trigger oidjoins
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index a4b354c9e6..8ad1f458d5 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..ed4b7c5d63
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,107 @@
+\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'
+);
+
+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
+
+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 acd0468a9d..8f4fa6e21c 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 relation')
+		('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}', '{}'),
+--TODO:				('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
+--TODO:    ('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 c6098dc95c..7db788735c 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 1149c6a839..e88c70d302 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -119,6 +119,23 @@ CREATE TABLE bububu(a int) \gdesc
 -- all on one line
 SELECT 3 AS x, 'Hello', 4 AS y, true AS "dirty\name" \gdesc \g
 
+
+-- \gencr
+-- (This just tests the parameter passing; there is no encryption here.)
+
+CREATE TABLE test_gencr (a int, b text);
+INSERT INTO test_gencr VALUES (1, 'one') \gencr
+SELECT * FROM test_gencr WHERE a = 1 \gencr
+
+INSERT INTO test_gencr VALUES ($1, $2) \gencr 2 'two'
+SELECT * FROM test_gencr WHERE a IN ($1, $2) \gencr 2 3
+
+-- test parse error
+SELECT * FROM test_gencr WHERE a = x \gencr
+-- test bind error
+SELECT * FROM test_gencr WHERE a = $1 \gencr
+
+
 -- \gexec
 
 create temporary table gexec_test(a int, b text, c date, d float);
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 5edc1f1f6e..0ffe45fd37 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: bb629c294bea533884a379eee5f8ed6307c17bf2
-- 
2.37.3

#36Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#35)
1 attachment(s)
Re: Transparent column encryption

New version with some merge conflicts resolved, and I have worked to
resolve several "TODO" items that I had noted in the code.

Show quoted text

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.)

Attachments:

v8-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v8-0001-Transparent-column-encryption.patchDownload
From 3bce601afe330769288347493a1524ecf86fe8c0 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 21 Sep 2022 17:17:56 -0400
Subject: [PATCH v8] 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/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/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/nls.mk                   |   2 +-
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  29 +
 .../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 +
 .../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 +
 129 files changed, 7605 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/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 8a1a9e9932c2..7968630aa043 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.
@@ -4686,6 +4903,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>
 
@@ -4736,6 +4993,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"/>
@@ -7766,6 +8024,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 f63c912e9713..7d71886f15e5 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>
 
@@ -4053,6 +4115,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>
@@ -5149,6 +5345,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>
 
@@ -5537,6 +5773,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>
 
@@ -7340,6 +7605,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 c08276bc0aae..83aab1cf0e3c 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 5d54074e0121..bdde5fc8c1a8 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 9b03579e6e01..e91b0b806daa 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -746,6 +746,9 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attgenerated - 1] = CharGetDatum(attrs->attgenerated);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attisdropped - 1] = BoolGetDatum(attrs->attisdropped);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attislocal - 1] = BoolGetDatum(attrs->attislocal);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = ObjectIdGetDatum(attrs->attcek);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attrealtypid - 1] = ObjectIdGetDatum(attrs->attrealtypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attencalg - 1] = Int16GetDatum(attrs->attencalg);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attinhcount - 1] = Int32GetDatum(attrs->attinhcount);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcollation - 1] = ObjectIdGetDatum(attrs->attcollation);
 		if (attoptions && attoptions[natts] != (Datum) 0)
diff --git a/src/backend/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/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 6c52edd9bee3..bf5cc2053683 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:
@@ -19326,3 +19342,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 724d076674e5..10ba67540121 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 d76c0af39405..5fbc43b89157 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 35eff28bd363..1dcda4baa6cf 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"
 
@@ -666,6 +667,8 @@ pg_analyze_and_rewrite_varparams(RawStmt *parsetree,
 								 const char *query_string,
 								 Oid **paramTypes,
 								 int *numParams,
+								 Oid **paramOrigTbls,
+								 AttrNumber **paramOrigCols,
 								 QueryEnvironment *queryEnv)
 {
 	Query	   *query;
@@ -680,7 +683,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.
@@ -1364,6 +1367,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.
@@ -1484,6 +1489,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 */
@@ -1514,6 +1521,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 */
@@ -1811,6 +1820,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;
@@ -2601,8 +2620,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 395f817fa8f1..ea4ef62be7b0 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 fcc5f6bd0564..47ba98eff382 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 67b6d9079ebe..da078523448b 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 *oprinfo);
 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 69ee939d4497..80bd29c2f33e 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 69ae027bd381..3ee7525e9638 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -91,6 +91,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;
@@ -152,6 +153,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},
@@ -422,6 +424,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)
@@ -647,6 +651,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 2873b662fb65..ec0199072707 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
@@ -3971,8 +4019,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 f3465adb8555..1d2f80c57205 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",
@@ -3518,6 +3566,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/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 6958306a7dcf..37a0034a7225 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 97f6894244dc..28dc80b0dded 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;
@@ -1587,7 +2018,9 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   NULL,
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1705,6 +2138,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;
@@ -1732,7 +2179,9 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   paramForceColumnEncryptions,
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1833,7 +2282,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;
@@ -1880,14 +2331,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;
 		}
 	}
@@ -1906,6 +2396,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1924,9 +2415,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
 		{
@@ -2380,12 +2916,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);
 }
@@ -3683,6 +4234,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)
 {
@@ -3868,6 +4430,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 bbfb55542df8..dbda7d9c7f50 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);
@@ -317,6 +319,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 &&
@@ -574,6 +592,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) ||
@@ -588,6 +608,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.
@@ -609,8 +644,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;
 	}
 
@@ -712,10 +749,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! */
@@ -1413,6 +1471,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.
@@ -1441,6 +1533,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.
@@ -2266,6 +2441,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/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/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/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/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 cbb99c7b9f94..113ec1392151 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 8cae20c0f582..163586c9f284 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: e59a67fb8fe1ac1408dc1858038f525a860d772b
-- 
2.37.3

#37Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#36)
1 attachment(s)
Re: Transparent column encryption

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

Show quoted text

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.)

Attachments:

v9-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v9-0001-Transparent-column-encryption.patchDownload
From 6a055b8d12d26c3728f517432cee5424628f02da Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
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

#38Andres Freund
andres@anarazel.de
In reply to: Peter Eisentraut (#37)
Re: Transparent column encryption

Hi,

On 2022-09-27 15:51:25 +0200, Peter Eisentraut wrote:

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

This fails on windows: https://cirrus-ci.com/task/6151847080624128

https://api.cirrus-ci.com/v1/artifact/task/6151847080624128/testrun/build/testrun/column_encryption/001_column_encryption/log/regress_log_001_column_encryption

psql error: stderr: 'OPENSSL_Uplink(00007FFC165CBD50,08): no OPENSSL_Applink'

Greetings,

Andres Freund

#39Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andres Freund (#38)
Re: Transparent column encryption

On 02.10.22 09:16, Andres Freund wrote:

On 2022-09-27 15:51:25 +0200, Peter Eisentraut wrote:

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

This fails on windows: https://cirrus-ci.com/task/6151847080624128

https://api.cirrus-ci.com/v1/artifact/task/6151847080624128/testrun/build/testrun/column_encryption/001_column_encryption/log/regress_log_001_column_encryption

psql error: stderr: 'OPENSSL_Uplink(00007FFC165CBD50,08): no OPENSSL_Applink'

What in the world is that about? What scant information I could find
suggests that it has something to do with building a "release" build
against an "debug" build of the openssl library, or vice versa. But
this patch doesn't introduce any use of openssl that we haven't seen before.

#40Andres Freund
andres@anarazel.de
In reply to: Peter Eisentraut (#39)
Re: Transparent column encryption

Hi,

On 2022-10-06 16:25:51 +0200, Peter Eisentraut wrote:

On 02.10.22 09:16, Andres Freund wrote:

On 2022-09-27 15:51:25 +0200, Peter Eisentraut wrote:

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

This fails on windows: https://cirrus-ci.com/task/6151847080624128

https://api.cirrus-ci.com/v1/artifact/task/6151847080624128/testrun/build/testrun/column_encryption/001_column_encryption/log/regress_log_001_column_encryption

psql error: stderr: 'OPENSSL_Uplink(00007FFC165CBD50,08): no OPENSSL_Applink'

What in the world is that about? What scant information I could find
suggests that it has something to do with building a "release" build against
an "debug" build of the openssl library, or vice versa. But this patch
doesn't introduce any use of openssl that we haven't seen before.

It looks to me that one needs to compile, in some form, openssl/applink.c and
link it to the application. No idea why that'd be required now and not
earlier.

Greetings,

Andres Freund

#41Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andres Freund (#40)
Re: Transparent column encryption

On 06.10.22 17:19, Andres Freund wrote:

psql error: stderr: 'OPENSSL_Uplink(00007FFC165CBD50,08): no OPENSSL_Applink'

What in the world is that about? What scant information I could find
suggests that it has something to do with building a "release" build against
an "debug" build of the openssl library, or vice versa. But this patch
doesn't introduce any use of openssl that we haven't seen before.

It looks to me that one needs to compile, in some form, openssl/applink.c and
link it to the application. No idea why that'd be required now and not
earlier.

I have figured this out. The problem is that on Windows you can't
reliably pass stdio FILE * handles between the application and OpenSSL.
To give the helpful places I found some Google juice, I'll mention them
here:

- https://github.com/edenhill/librdkafka/pull/3602
- https://github.com/edenhill/librdkafka/issues/3554
- https://www.mail-archive.com/openssl-users@openssl.org/msg91029.html

#42Mark Woodward
woodwardm@google.com
In reply to: Peter Eisentraut (#41)
Re: Transparent column encryption

If memory serves me correctly, if you statically link openssl this will
work. If you are using ssl in a DLL, I believe that the DLL has its own
"c-library" and its own heap.

On Thu, Oct 13, 2022 at 9:43 AM Peter Eisentraut <
peter.eisentraut@enterprisedb.com> wrote:

Show quoted text

On 06.10.22 17:19, Andres Freund wrote:

psql error: stderr: 'OPENSSL_Uplink(00007FFC165CBD50,08): no

OPENSSL_Applink'

What in the world is that about? What scant information I could find
suggests that it has something to do with building a "release" build

against

an "debug" build of the openssl library, or vice versa. But this patch
doesn't introduce any use of openssl that we haven't seen before.

It looks to me that one needs to compile, in some form,

openssl/applink.c and

link it to the application. No idea why that'd be required now and not
earlier.

I have figured this out. The problem is that on Windows you can't
reliably pass stdio FILE * handles between the application and OpenSSL.
To give the helpful places I found some Google juice, I'll mention them
here:

- https://github.com/edenhill/librdkafka/pull/3602
- https://github.com/edenhill/librdkafka/issues/3554
- https://www.mail-archive.com/openssl-users@openssl.org/msg91029.html

#43Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#37)
1 attachment(s)
Re: Transparent column encryption

Here is an updated version with the tests on Windows working again, and
some typos fixed.

Show quoted text

On 27.09.22 15:51, Peter Eisentraut wrote:

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.)

Attachments:

v10-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v10-0001-Transparent-column-encryption.patchDownload
From 8a7bf9657a65a840a2fc9a0e6a604399344c89d9 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Fri, 14 Oct 2022 08:20:55 +0200
Subject: [PATCH v10] 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 asymmetric 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     | 782 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 580 ++++++++++++-
 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                | 186 +++++
 .../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, 7639 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 b030b36002f4..bb3c6a685f4a 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5359,4 +5359,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..898e4f7f93b0 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 asymmetric 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 on 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 3c9bd3d67307..13abf456b125 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>
@@ -3028,6 +3145,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>
 
@@ -3878,6 +4046,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>
 
@@ -4053,12 +4243,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>
 
@@ -4584,6 +4799,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
@@ -4591,6 +4807,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.
@@ -4701,6 +4918,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>
 
@@ -4751,6 +5008,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"/>
@@ -7783,6 +8041,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 5fdd429e05d3..2aae66ce8d2f 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 7f3e64b5ae61..8f1f2fae6a00 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 5b49cc5a0989..fbd97ab2946f 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 20135ef1b009..a30d7872f48e 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);
 
@@ -12677,6 +12690,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:
@@ -19312,3 +19328,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 e555fb315019..46be4d106c51 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 fd5796f1b9e1..8a7760594509 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 94d5142a4a06..64b97f2a76bc 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
@@ -16726,6 +16825,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16789,6 +16889,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17272,6 +17373,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17360,6 +17462,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 27dee29f420b..a68e9fe0225d 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 247d0816ad81..38a5d8f1c038 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 474ab476f5fb..33e871ff0608 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 bd9b066e4eb8..9dfe09119123 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"
@@ -226,6 +227,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);
@@ -385,6 +388,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},
@@ -677,6 +681,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");
 
@@ -1022,6 +1029,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"
@@ -5518,6 +5526,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
@@ -8108,6 +8248,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;
@@ -8217,17 +8360,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);
@@ -8246,6 +8401,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");
@@ -8307,6 +8465,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));
@@ -8335,6 +8496,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));
@@ -9859,6 +10032,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;
@@ -13252,6 +13431,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
@@ -15335,6 +15661,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)
@@ -17899,6 +18241,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 ab613dd49e0a..5887ffc4bd1c 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 4f310a8019dc..a47575a3639a 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1230,6 +1230,14 @@ SendQuery(const char *query)
 	/* reset \gdesc trigger */
 	pset.gdesc_flag = false;
 
+	/* reset \gencr trigger */
+	pset.gencr_flag = false;
+	for (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;
 
@@ -1397,7 +1405,36 @@ ExecQueryAndProcessResults(const char *query,
 	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 584d9d5ae642..1e9a19e4e751 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",
@@ -3519,6 +3567,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 68bb032d3ea5..904723a5cce1 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8036,9 +8036,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',
@@ -11823,4 +11823,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 633e7671b3ea..7556d00e348f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -687,6 +687,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? */
@@ -1865,6 +1866,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2057,6 +2061,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 ccc927851cb9..24a683f4ae0e 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..ee039f66a5b9
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,782 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = 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;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("could not open file \"%s\": %m\n"), cmkfilename);
+		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;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &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);
+	BIO_free(bio);
+
+	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..c0f9f36250e6
--- /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, const char *cmkfilename, 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..bf73202cb855 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,369 @@ 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, const char *cmkfilename, 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;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				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 +1621,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 +1677,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 +1949,9 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   NULL,
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1649,6 +2069,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 +2110,9 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   paramForceColumnEncryptions,
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1777,7 +2213,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 +2262,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 +2327,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1868,9 +2346,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 +2831,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 +4150,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 +4346,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 8e696f1183cf..a3e2d2e98ba6 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -27,6 +27,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 017f729d435d..fea613dd634b 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -34,3 +34,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..b8f07ce52d01
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,186 @@
+# 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');
+}
+
+TODO: {
+	local $TODO = 'path not being passed correctly on Windows' if $windows_os;
+
+	local $ENV{'PGCMKLOOKUP'} = "*=run:perl ./test_run_decrypt.pl '${PostgreSQL::Test::Utils::tmp_check}' %k %a '%b'";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		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..07b1fb1bb380
--- /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 "system failed: $?";
+
+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 dadd58e8b9c3..951ac1e8b4d2 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 (
@@ -106,7 +108,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
@@ -326,6 +329,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
@@ -381,6 +402,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[]),
@@ -403,6 +432,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}', '{}'),
@@ -448,59 +480,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
@@ -514,6 +549,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
@@ -549,6 +586,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
@@ -579,6 +619,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
@@ -630,5 +671,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: 5263c6b095c9bac2a4a744b72425e6690121c79d
-- 
2.37.3

In reply to: Peter Eisentraut (#43)
Re: Transparent column encryption

Hi,

I did a review of the documentation and usability.

# Applying patch

The patch applied on top of f13b2088fa2 without trouble. Notice a small
warning during compilation:

colenccmds.c:134:27: warning: ‘encval’ may be used uninitialized

A simple fix could be:

    +++ b/src/backend/commands/colenccmds.c
    @@ -119,2 +119,3
                    encval = defGetString(encvalEl);
    +               *encval_p = encval;
            }
    @@ -132,4 +133,2
                    *alg_p = alg;
    -       if (encval_p)
    -               *encval_p = encval;
     }

# Documentation

* In page "create_column_encryption_key.sgml", both encryption algorithms for
a CMK are declared as the default one:

    +     <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>

As far as I understand the code, I suppose RSAES_OAEP_SHA_1 should be the
default.

I believe two information should be clearly shown to user somewhere in
chapter 5.5 instead of being buried deep in documentation:

* «COPY does not support column decryption», currently buried in pg_dump page
* «When transparent column encryption is enabled, the client encoding must
match the server encoding», currently buried in the protocol description
page.

* In the libpq doc of PQexecPrepared2, "paramForceColumnEncryptions" might
deserve a little more detail about the array format, like «0 means "don't
enforce" and anything else enforce the encryption is enabled on this
column». By the way, maybe this array could be an array of boolean?

* In chapter 55.2.5 (protocol-flow) is stated: «when column encryption is
used, the plaintext is always in text format (not binary format).». Does it
means parameter "resultFormat" in "PQexecPrepared2" should always be 0? If
yes, is it worth keeping this argument? Moreover, this format constraint
should probably be explained in the libpq page as well.

# Protocol

* In the ColumnEncryptionKey message, it seems the field holding the length
key material is redundant with the message length itself, as all other
fields have a known size. The key material length is the message length -
(4+4+4+2). For comparison, the AuthenticationSASLContinue message has a
variable data size but rely only on the message length without additional
field.

* I wonder if encryption related fields in ParameterDescription and
RowDescription could be optional somehow? The former might be quite large
when using a lot of parameters (like, imagine a large and ugly
"IN($1...$N)"). On the other hand, these messages are not sent in high
frequency anyway...

# libpq

Would it be possible to have an encryption-ready PQexecParams() equivalent
of what PQprepare/describe/exec do?

# psql

* You already mark \d in the TODO. But having some psql command to list the
known CEK/CMK might be useful as well.

* about write queries using psql, would it be possible to use psql
parameters? Eg.:

=> \set c1 myval
=> INSERT INTO mytable VALUES (:'c1') \gencr

# Manual tests

* The lookup error message is shown twice for some reason:

=> select * from test_tce;
no CMK lookup found for realm ""

no CMK lookup found for realm ""

It might worth adding the column name and the CMK/CEK names related to each
error message? Last, notice the useless empty line between messages.

* When "DROP IF EXISTS" a missing CEK or CMK, the command raise an
"unrecognized object type":

=> DROP COLUMN MASTER KEY IF EXISTS noexists;
ERROR: unrecognized object type: 10
=> DROP COLUMN ENCRYPTION KEY IF EXISTS noexists;
ERROR: unrecognized object type: 8

* I was wondering what "pg_typeof" should return. It currently returns
"pg_encrypted_*". If this is supposed to be transparent from the client
perspective, shouldn't it return "attrealtypid" when the field is encrypted?

* any reason to not support altering the CMK realm?

This patch is really interesting and would be a nice addition to the core.

Thanks!

Regards,

#45Frédéric Yhuel
frederic.yhuel@dalibo.com
In reply to: Jehan-Guillaume de Rorthais (#44)
Re: Transparent column encryption

Hi,

Here are a few more things I noticed :

If a CEK is encrypted with cmk1 and cmk2, but cmk1 isn't found on the
client,the following error is printed twice for the very first SELECT
statement:

could not open file "/path/to/cmk1.pem": No such file or directory

...and nothing is returned. The next queries in the same session would
work correctly (cmk2 is used for the decryption of the CEK). An INSERT
statement si handled properly, though : one (and only one) error
message, and line actually inserted in all cases).

For example :

postgres=# SELECT * FROM customers ;
could not open file "/path/to/cmk1.pem": No such file or directory

could not open file "/path/to/cmk1.pem": No such file or directory

postgres=# SELECT * FROM customers ;
id | name | creditcard_num
----+-------+-----------------
1 | toto | 546843351354245
2 | babar | 546843351354245

<close and open new psql session>

postgres=# INSERT INTO customers (id, name, creditcard_num) VALUES
($1, $2, $3) \gencr '3' 'toto' '546888351354245';
could not open file "/path/to/cmk1.pem": No such file or directory

INSERT 0 1
postgres=# SELECT * FROM customers ;
id | name | creditcard_num
----+-------+-----------------
1 | toto | 546843351354245
2 | babar | 546843351354245
3 | toto | 546888351354245

From the documentation of CREATE COLUMN MASTER KEY, it looks like the
REALM is optional, but both
CREATE COLUMN MASTER KEY cmk1;
and
CREATE COLUMN MASTER KEY cmk1 WITH ();
returns a syntax error.

About AEAD, the documentation says :

The “associated data” in these algorithms consists of 4 bytes: The

ASCII letters P and G (byte values 80 and 71), followed by the algorithm
ID as a 16-bit unsigned integer in network byte order.

My guess is that it serves no real purpose, did I misunderstand ?

#46Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#43)
1 attachment(s)
Re: Transparent column encryption

Here is another updated patch. Some preliminary work was committed,
which allowed this patch to get a bit smaller. I have incorporated some
recent reviews, and also fixed some issues pointed out by recent CI
additions (address sanitizer etc.).

The psql situation in this patch is temporary: It still has the \gencr
command from previous versions, but I plan to fold this into the new
\bind command.

Show quoted text

On 14.10.22 08:27, Peter Eisentraut wrote:

Here is an updated version with the tests on Windows working again, and
some typos fixed.

On 27.09.22 15:51, Peter Eisentraut wrote:

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.)

Attachments:

v11-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v11-0001-Transparent-column-encryption.patchDownload
From f5cd86d93f58a350b6aeb2d332e4fc1f6d97d0f2 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 23 Nov 2022 19:09:10 +0100
Subject: [PATCH v11] 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 asymmetric 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                       | 278 +++++++
 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                  |  12 +
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |   3 +
 src/backend/catalog/objectaddress.c           | 213 +++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  15 +
 src/backend/commands/colenccmds.c             | 355 ++++++++
 src/backend/commands/dropcmds.c               |   9 +
 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              |  35 +-
 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          |   2 +
 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              |  48 ++
 src/bin/psql/command.c                        |  31 +
 src/bin/psql/common.c                         |  37 +-
 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/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             |  25 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 760 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 582 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 146 +++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  22 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 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           |  31 +
 src/test/column_encryption/meson.build        |  23 +
 .../t/001_column_encryption.pl                | 188 +++++
 .../column_encryption/t/002_cmk_rotation.pl   | 110 +++
 src/test/column_encryption/test_client.c      | 210 +++++
 .../column_encryption/test_run_decrypt.pl     |  43 +
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 147 ++++
 src/test/regress/expected/object_address.out  |  45 +-
 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    | 126 +++
 src/test/regress/sql/object_address.sql       |  17 +-
 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, 7460 insertions(+), 148 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 9ed2b020b7d9..0cf16620ee1c 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>
@@ -2467,6 +2516,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 b030b36002f4..bb3c6a685f4a 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5359,4 +5359,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..898e4f7f93b0 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 asymmetric 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 on 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 93fb149d9a26..7defd96fed90 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,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 f9558dec3b62..485a1c61e3d4 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>
@@ -3028,6 +3145,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>
 
@@ -3878,6 +4046,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>
 
@@ -4059,6 +4249,31 @@ <title>Retrieving Query Result Information</title>
      </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>
 
@@ -4584,6 +4799,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
@@ -4591,6 +4807,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.
@@ -4701,6 +4918,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>
 
@@ -4751,6 +5008,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"/>
@@ -7783,6 +8041,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 5fdd429e05d3..2aae66ce8d2f 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..a33eb3fcef86
--- /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></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 c98223b2a51c..e4bf54e2e630 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 | DEFAULT } ] [ 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 | DEFAULT } ] [ 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> ... ] }
     [, ... ]
@@ -351,6 +351,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 d31cf17f5def..cd9c64251ce4 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -2291,6 +2291,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>
 
@@ -4014,6 +4036,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 7857f55e24a4..32b131781f92 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 3c9f8e60ad22..1c1aab7f4c4a 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.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"
@@ -3596,6 +3598,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:
@@ -3626,6 +3631,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;
@@ -3736,6 +3747,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:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 7f3e64b5ae61..8f1f2fae6a00 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 bdd413f01b05..b31c2528fc8b 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 fe97fbf79dcd..1dfe4b69265e 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
@@ -1029,6 +1086,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:
@@ -1108,6 +1167,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));
@@ -1293,6 +1367,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);
@@ -2253,6 +2337,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)
@@ -2327,6 +2412,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:
@@ -2357,6 +2444,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));
@@ -2480,6 +2568,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   NameListToString((castNode(ObjectWithArgs, object))->objname));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_DATABASE:
 		case OBJECT_EVENT_TRIGGER:
 		case OBJECT_EXTENSION:
@@ -2572,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3037,6 +3128,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;
@@ -4461,6 +4594,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);
@@ -4926,6 +5071,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 10b6fe19a2c3..db1ada3909c9 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..93421ce29679
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,355 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 "catalog/pg_database.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 = InvalidOid;
+	int16		alg;
+	char	   *encval = NULL;
+
+	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 = object_aclcheck(DatabaseRelationId, 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 (!object_ownercheck(ColumnEncKeyRelationId, 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 = object_aclcheck(DatabaseRelationId, 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/dropcmds.c b/src/backend/commands/dropcmds.c
index db906f530ec0..ccb369b1af6f 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,14 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+			name = strVal(object);
+			break;
+		case OBJECT_CMK:
+			msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+			name = strVal(object);
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +511,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index a3bdc5db0735..c10732a56ed0 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 9e29584d93ef..f34c5ff25a53 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 845208d662ba..4c80e87c4a4a 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"
@@ -637,6 +638,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);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +938,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);
 
@@ -12696,6 +12709,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:
@@ -19331,3 +19347,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 00d8d54d820a..c67497257b69 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 fd5796f1b9e1..8a7760594509 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 af8620ceb7ce..140a14b7a782 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3982,6 +3982,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 9384214942aa..fd6772f97027 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -279,7 +279,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
@@ -419,6 +419,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
@@ -592,6 +593,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
@@ -690,8 +692,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
@@ -714,7 +716,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
@@ -942,6 +944,7 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -3680,14 +3683,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;
@@ -3696,8 +3700,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;
@@ -3754,6 +3758,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -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
@@ -16771,6 +16870,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16834,6 +16934,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17317,6 +17418,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17405,6 +17507,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 e80876aa25e0..aab47b3cfedb 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,10 +152,24 @@ variable_paramref_hook(ParseState *pstate, ParamRef *pref)
 	{
 		/* Need to enlarge param array */
 		if (*parstate->paramTypes)
+		{
 			*parstate->paramTypes = repalloc0_array(*parstate->paramTypes, Oid,
 													*parstate->numParams, paramno);
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc0_array(*parstate->paramOrigTbls, Oid,
+														   *parstate->numParams, paramno);
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc0_array(*parstate->paramOrigCols, AttrNumber,
+														   *parstate->numParams, paramno);
+		}
 		else
+		{
 			*parstate->paramTypes = palloc0_array(Oid, paramno);
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc0_array(Oid, paramno);
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc0_array(AttrNumber, paramno);
+		}
 		*parstate->numParams = paramno;
 	}
 
@@ -256,6 +277,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 8e0d6fd01f1f..c92f783b5cd9 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 c83cc8cc6cd3..225ed005e131 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2244,12 +2244,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 3082093d1eab..8b48b1539c5e 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 247d0816ad81..38a5d8f1c038 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 94ca8e12303d..49f73ca4e828 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
  *
@@ -3683,3 +3705,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 cc943205d342..fe2609a97d51 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 474ab476f5fb..33e871ff0608 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 f39c0fa36fdc..971ea4b6c736 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3408,6 +3408,8 @@ _getObjectDescription(PQExpBuffer buf, const 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, "FOREIGN TABLE") == 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 da427f4d4a17..3c7428a60d07 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"
@@ -226,6 +227,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);
@@ -385,6 +388,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},
@@ -677,6 +681,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");
 
@@ -1022,6 +1029,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"
@@ -5518,6 +5526,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
@@ -8108,6 +8248,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;
@@ -8217,17 +8360,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);
@@ -8246,6 +8401,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");
@@ -8307,6 +8465,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));
@@ -8335,6 +8496,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));
@@ -9859,6 +10032,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;
@@ -13252,6 +13431,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
@@ -15335,6 +15661,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)
@@ -17899,6 +18241,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 a87262e33357..695a1cf3079b 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 8dc1f0eccb5d..78d961a8b9c7 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
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 7672ed9e9d56..712eed899efd 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -99,6 +99,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);
@@ -353,6 +354,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)
@@ -1529,6 +1532,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 b989d792aa75..a72f132eaed9 100644
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -1240,6 +1240,14 @@ SendQuery(const char *query)
 	/* reset \gdesc trigger */
 	pset.gdesc_flag = false;
 
+	/* reset \gencr trigger */
+	pset.gencr_flag = false;
+	for (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;
 
@@ -1407,7 +1415,34 @@ ExecQueryAndProcessResults(const char *query,
 	if (timing)
 		INSTR_TIME_SET_CURRENT(before);
 
-	if (pset.bind_flag)
+	// FIXME
+	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 if (pset.bind_flag)
 		success = PQsendQueryParams(pset.db, query, pset.bind_nparams, NULL, (const char * const *) pset.bind_params, NULL, NULL, 0);
 	else
 		success = PQsendQuery(pset.db, query);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2eae519b1dd8..843f395c3222 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 b4e0ec2687fd..2630958a8d5b 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -196,6 +196,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");
@@ -413,6 +414,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 3fce71b85fe4..2aa783f45f7e 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		bind_flag;		/* one-shot request to use extended query protocol */
 	int			bind_nparams;	/* number of parameters */
@@ -137,6 +141,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 13014f074f40..332cf360e523 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1175,6 +1175,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.
@@ -1208,6 +1218,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
@@ -1699,7 +1711,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",
@@ -1907,6 +1919,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");
@@ -2828,6 +2856,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",
@@ -3553,6 +3601,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 f15aa2dbb1b0..467fa5a75281 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8067,9 +8067,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',
@@ -11854,4 +11854,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 7caff62af7f3..78083a08c57c 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -687,6 +687,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? */
@@ -1866,6 +1867,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2058,6 +2062,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 957ee18d8498..a6084574c583 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/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 f88d672c6c8a..48b41f9709fe 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}
@@ -2422,6 +2430,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;
@@ -4029,6 +4038,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..0bd3ee1f881d
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,760 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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)
+#define libpq_append_conn_error(conn, ...) appendPQExpBuffer(&(conn)->errorMessage, __VA_ARGS__)
+
+#endif							/* TEST_ENCRYPT */
+
+
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = 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:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								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);
+	BIO_free(bio);
+
+	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;
+	int64		al;
+	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);
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	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)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "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)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								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))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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..c0f9f36250e6
--- /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, const char *cmkfilename, 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 da229d632a1e..53f91358b568 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);
@@ -1183,6 +1188,375 @@ 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, size_t b64datalen)
+{
+	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':
+					appendBinaryPQExpBuffer(&buf, b64data, b64datalen);
+					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, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', 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)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID", 7);
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				int			enclen;
+				char	   *enc;
+				char	   *command;
+				FILE	   *fp;
+				char		buf[4096];
+				int			rc;
+
+				enclen = pg_b64_enc_len(fromlen);
+				enc = malloc(enclen + 1);
+				if (!enc)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				enclen = pg_b64_encode((const char *) from, fromlen, enc, enclen);
+				if (enclen < 0)
+				{
+					libpq_append_conn_error(conn, "base64 encoding failed");
+					free(enc);
+					goto fail;
+				}
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, enc, enclen);
+				free(enc);
+				fp = popen(command, "r");
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					goto fail;
+				}
+				if (fgets(buf, sizeof(buf), fp))
+				{
+					int			linelen;
+					int			declen;
+					char	   *dec;
+
+					linelen = strlen(buf);
+					if (buf[linelen - 1] == '\n')
+					{
+						buf[linelen - 1] = '\0';
+						linelen--;
+					}
+					declen = pg_b64_dec_len(linelen);
+					dec = malloc(declen);
+					if (!dec)
+					{
+						libpq_append_conn_error(conn, "out of memory");
+						free(command);
+						goto fail;
+					}
+					declen = pg_b64_decode(buf, linelen, dec, declen);
+					if (declen < 0)
+					{
+						libpq_append_conn_error(conn, "base64 decoding failed");
+						free(dec);
+						free(command);
+						goto fail;
+					}
+					result = (unsigned char *) dec;
+					*tolen = declen;
+				}
+				else
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					goto fail;
+				}
+				free(command);
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", 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
@@ -1251,6 +1625,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;
@@ -1258,6 +1681,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;
@@ -1524,7 +1948,9 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   NULL,
+						   resultFormat,
+						   NULL);
 }
 
 /*
@@ -1639,6 +2065,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;
@@ -1664,7 +2104,9 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   paramForceColumnEncryptions,
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2204,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;
@@ -1809,14 +2253,51 @@ 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))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					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)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					format = 1;
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,6 +2316,7 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
 			if (paramFormats && paramFormats[i] != 0)
 			{
@@ -1852,9 +2334,52 @@ 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)
+				{
+					libpq_append_conn_error(conn, "column encryption key not found");
+					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
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2815,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);
 }
@@ -3577,6 +4117,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)
 {
@@ -3762,6 +4313,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 364bad2b882c..280f7dc52d1b 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);
@@ -297,6 +299,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 &&
@@ -547,6 +565,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) ||
@@ -561,6 +581,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.
@@ -582,8 +617,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;
 	}
 
@@ -685,10 +722,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! */
@@ -1468,6 +1526,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.
@@ -2286,6 +2427,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 512762f99917..9783ba773698 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);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 8e696f1183cf..a3e2d2e98ba6 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -27,6 +27,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 4df544ecef56..0c36aa5f32b6 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-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_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext pqInternalNotice:2
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 017f729d435d..fea613dd634b 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -34,3 +34,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..76a153e33063
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# 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
+
+export OPENSSL
+
+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..84cfa84e12f8
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,23 @@
+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',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+    },
+  },
+}
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..bc82985309e0
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,188 @@
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+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');
+}
+
+TODO: {
+	local $TODO = 'path not being passed correctly on Windows' if $windows_os;
+
+	local $ENV{'PGCMKLOOKUP'} = "*=run:perl ./test_run_decrypt.pl '${PostgreSQL::Test::Utils::tmp_check}' %k %a '%b'";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		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..b90521373e7d
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,110 @@
+# 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 $openssl = $ENV{OPENSSL};
+
+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..3ebf503062db
--- /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, false};
+
+	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..caf63f9c9eca
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,43 @@
+#!/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';
+
+my $openssl = $ENV{OPENSSL};
+
+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 "system failed: $?";
+
+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..f1dd4f44a0ed
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,147 @@
+\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 ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 25c174f27503..46ce05d79047 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,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 (
@@ -107,7 +109,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
@@ -327,6 +330,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
@@ -382,6 +403,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
 -- Make sure that NULL handling is correct.
 \pset null 'NULL'
 -- Temporarily disable fancy output, so as future additions never create
@@ -409,6 +438,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}', '{}'),
@@ -505,6 +537,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|NULL|addr_cmk|addr_cmk|t
+column encryption key|NULL|addr_cek|addr_cek|t
+column encryption key data|NULL|NULL|of addr_cek for addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +552,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
@@ -547,6 +584,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
@@ -634,5 +674,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|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 5bdae290dcec..b7a3d199d317 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -268,6 +268,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 7c7adbc0045a..bb3371b18e5a 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1426,11 +1426,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 9a139f1e2487..775253b50f6c 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..1322c2e775a5
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,126 @@
+\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 ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d54..9dcc614d9099 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,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 (
@@ -99,7 +101,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
@@ -144,6 +147,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}', '{}');
 
 -- Make sure that NULL handling is correct.
 \pset null 'NULL'
@@ -174,6 +181,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}', '{}'),
@@ -228,6 +238,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;
 
@@ -247,6 +259,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
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 8732017e51e9..e51b10d6c4d5 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -133,6 +133,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: b425bf0081386a544e1faf872a75da69a971e173
-- 
2.38.1

#47Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jehan-Guillaume de Rorthais (#44)
Re: Transparent column encryption

On 28.10.22 12:16, Jehan-Guillaume de Rorthais wrote:

I did a review of the documentation and usability.

I have incorporated some of your feedback into the v11 patch I just posted.

# Applying patch

The patch applied on top of f13b2088fa2 without trouble. Notice a small
warning during compilation:

colenccmds.c:134:27: warning: ‘encval’ may be used uninitialized

A simple fix could be:

+++ b/src/backend/commands/colenccmds.c
@@ -119,2 +119,3
encval = defGetString(encvalEl);
+               *encval_p = encval;
}
@@ -132,4 +133,2
*alg_p = alg;
-       if (encval_p)
-               *encval_p = encval;
}

fixed

# Documentation

* In page "create_column_encryption_key.sgml", both encryption algorithms for
a CMK are declared as the default one:

+     <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>

As far as I understand the code, I suppose RSAES_OAEP_SHA_1 should be the
default.

fixed

I believe two information should be clearly shown to user somewhere in
chapter 5.5 instead of being buried deep in documentation:

* «COPY does not support column decryption», currently buried in pg_dump page
* «When transparent column encryption is enabled, the client encoding must
match the server encoding», currently buried in the protocol description
page.

* In the libpq doc of PQexecPrepared2, "paramForceColumnEncryptions" might
deserve a little more detail about the array format, like «0 means "don't
enforce" and anything else enforce the encryption is enabled on this
column». By the way, maybe this array could be an array of boolean?

* In chapter 55.2.5 (protocol-flow) is stated: «when column encryption is
used, the plaintext is always in text format (not binary format).». Does it
means parameter "resultFormat" in "PQexecPrepared2" should always be 0? If
yes, is it worth keeping this argument? Moreover, this format constraint
should probably be explained in the libpq page as well.

I will keep these suggestions around. Some of these things will
probably change again, so I'll make sure to update the documentation
when I touch it again.

# Protocol

* In the ColumnEncryptionKey message, it seems the field holding the length
key material is redundant with the message length itself, as all other
fields have a known size. The key material length is the message length -
(4+4+4+2). For comparison, the AuthenticationSASLContinue message has a
variable data size but rely only on the message length without additional
field.

I find that weird, though. An explicit length seems better. Things
like AuthenticationSASLContinue only work if they have exactly one
variable-length data item.

* I wonder if encryption related fields in ParameterDescription and
RowDescription could be optional somehow? The former might be quite large
when using a lot of parameters (like, imagine a large and ugly
"IN($1...$N)"). On the other hand, these messages are not sent in high
frequency anyway...

They are only used if you turn on the column_encryption protocol option.
Or did you mean make them optional even then?

# libpq

Would it be possible to have an encryption-ready PQexecParams() equivalent
of what PQprepare/describe/exec do?

I plan to do that.

# psql

* You already mark \d in the TODO. But having some psql command to list the
known CEK/CMK might be useful as well.

right

* about write queries using psql, would it be possible to use psql
parameters? Eg.:

=> \set c1 myval
=> INSERT INTO mytable VALUES (:'c1') \gencr

No, because those are resolved by psql before libpq sees them.

# Manual tests

* The lookup error message is shown twice for some reason:

=> select * from test_tce;
no CMK lookup found for realm ""

no CMK lookup found for realm ""

It might worth adding the column name and the CMK/CEK names related to each
error message? Last, notice the useless empty line between messages.

I'll look into that.

* When "DROP IF EXISTS" a missing CEK or CMK, the command raise an
"unrecognized object type":

=> DROP COLUMN MASTER KEY IF EXISTS noexists;
ERROR: unrecognized object type: 10
=> DROP COLUMN ENCRYPTION KEY IF EXISTS noexists;
ERROR: unrecognized object type: 8

fixed

* I was wondering what "pg_typeof" should return. It currently returns
"pg_encrypted_*". If this is supposed to be transparent from the client
perspective, shouldn't it return "attrealtypid" when the field is encrypted?

Interesting question. Need to think about it. I'm not sure what the
purpose of pg_typeof really is. The only use I can recall is for pgTAP.

* any reason to not support altering the CMK realm?

This could be added. I have that in my notes.

In reply to: Peter Eisentraut (#47)
Re: Transparent column encryption

On Wed, 23 Nov 2022 19:45:06 +0100
Peter Eisentraut <peter.eisentraut@enterprisedb.com> wrote:

On 28.10.22 12:16, Jehan-Guillaume de Rorthais wrote:

[...]

* I wonder if encryption related fields in ParameterDescription and
RowDescription could be optional somehow? The former might be quite
large when using a lot of parameters (like, imagine a large and ugly
"IN($1...$N)"). On the other hand, these messages are not sent in high
frequency anyway...

They are only used if you turn on the column_encryption protocol option.
Or did you mean make them optional even then?

I meant even when column_encryption is turned on.

Regards,

#49Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#46)
1 attachment(s)
Re: Transparent column encryption

On 23.11.22 19:39, Peter Eisentraut wrote:

Here is another updated patch.  Some preliminary work was committed,
which allowed this patch to get a bit smaller.  I have incorporated some
recent reviews, and also fixed some issues pointed out by recent CI
additions (address sanitizer etc.).

The psql situation in this patch is temporary: It still has the \gencr
command from previous versions, but I plan to fold this into the new
\bind command.

I made a bit of progress with this now, based on recent reviews:

- Cleaned up the libpq API. PQexecParams() now supports column
encryption transparently.
- psql \bind can be used; \gencr is removed.
- Added psql \dcek and \dcmk commands.
- ALTER COLUMN MASTER KEY to alter realm.

Attachments:

v12-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v12-0001-Transparent-column-encryption.patchDownload
From 9827d789d73d0ceeb1dd888f8ca7a84261608165 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Mon, 28 Nov 2022 14:31:20 +0100
Subject: [PATCH v12] 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 asymmetric 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
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.

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).  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.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this
transparently if the \bind command is used.

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.  This
functionality is in principle available to all prepared-statement
variants, not only protocol-level.  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.

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                         | 372 +++++++++
 doc/src/sgml/glossary.sgml                    |  23 +
 doc/src/sgml/libpq.sgml                       | 290 +++++++
 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 | 124 +++
 .../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                |  39 +
 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                  |  12 +
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |   3 +
 src/backend/catalog/objectaddress.c           | 213 +++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  15 +
 src/backend/commands/colenccmds.c             | 423 ++++++++++
 src/backend/commands/dropcmds.c               |   9 +
 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                     | 139 +++-
 src/backend/parser/parse_param.c              |  35 +-
 src/backend/parser/parse_target.c             |   6 +
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  57 +-
 src/backend/tcop/utility.c                    |  55 +-
 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          |   2 +
 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              |  48 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 157 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 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             |  26 +
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  29 +
 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/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             |  25 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 760 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 606 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 146 +++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 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           |  31 +
 src/test/column_encryption/meson.build        |  23 +
 .../t/001_column_encryption.pl                | 223 +++++
 .../column_encryption/t/002_cmk_rotation.pl   | 110 +++
 src/test/column_encryption/test_client.c      | 110 +++
 .../column_encryption/test_run_decrypt.pl     |  60 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 166 ++++
 src/test/regress/expected/object_address.out  |  45 +-
 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/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    | 146 ++++
 src/test/regress/sql/object_address.sql       |  17 +-
 src/test/regress/sql/prepare.sql              |   2 +-
 src/test/regress/sql/type_sanity.sql          |   2 +
 133 files changed, 7630 insertions(+), 148 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 9ed2b020b7d9..0cf16620ee1c 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>
@@ -2467,6 +2516,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 b030b36002f4..bb3c6a685f4a 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5359,4 +5359,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..0478c630ada2 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1211,6 +1211,378 @@ <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 asymmetric 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>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO ssn (id, name, ssn) VALUES ($1, $2, $3)", 3, NULL, values, NULL, NULL, 0);
+
+/* 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>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO ssn (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</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 on 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 93fb149d9a26..7defd96fed90 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,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 f9558dec3b62..4baea0eb2df4 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>
@@ -2864,6 +2981,25 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this byte to one to force encryption.
+            For example, use the C code literal <literal>0x10</literal> to
+            specify text format with forced encryption.  If the array pointer
+            is null then encryption is not forced for any parameter.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter 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.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -3028,6 +3164,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</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 *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> 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>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4052,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>
 
@@ -4059,6 +4255,31 @@ <title>Retrieving Query Result Information</title>
      </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-PQexecPreparedDescribed"/> 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>
 
@@ -4584,6 +4805,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4813,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4870,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4931,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</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 PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> 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-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5020,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7783,6 +8053,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 5fdd429e05d3..2aae66ce8d2f 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..370347ec9ce7
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,124 @@
+<!--
+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> ( REALM = <replaceable>realm</replaceable> )
+
+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>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </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..a33eb3fcef86
--- /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></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 c98223b2a51c..e4bf54e2e630 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 | DEFAULT } ] [ 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 | DEFAULT } ] [ 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> ... ] }
     [, ... ]
@@ -351,6 +351,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..7f4d867672d3 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 ... \bind ... \g 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 d3dd638b148a..45aa3943f8b7 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry>
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry>
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry>
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4015,6 +4043,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 7857f55e24a4..32b131781f92 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 3c9f8e60ad22..1c1aab7f4c4a 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.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"
@@ -3596,6 +3598,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:
@@ -3626,6 +3631,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;
@@ -3736,6 +3747,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:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 7f3e64b5ae61..8f1f2fae6a00 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 bdd413f01b05..b31c2528fc8b 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 fe97fbf79dcd..1dfe4b69265e 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
@@ -1029,6 +1086,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:
@@ -1108,6 +1167,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));
@@ -1293,6 +1367,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);
@@ -2253,6 +2337,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)
@@ -2327,6 +2412,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:
@@ -2357,6 +2444,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));
@@ -2480,6 +2568,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   NameListToString((castNode(ObjectWithArgs, object))->objname));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_DATABASE:
 		case OBJECT_EVENT_TRIGGER:
 		case OBJECT_EXTENSION:
@@ -2572,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3037,6 +3128,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;
@@ -4461,6 +4594,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);
@@ -4926,6 +5071,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 10b6fe19a2c3..db1ada3909c9 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..d873c86075ac
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,423 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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 "catalog/pg_database.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 = InvalidOid;
+	int16		alg;
+	char	   *encval = NULL;
+
+	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, "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 = object_aclcheck(DatabaseRelationId, 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 (!object_ownercheck(ColumnEncKeyRelationId, 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 = object_aclcheck(DatabaseRelationId, 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;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, stmt->cmkname);
+
+	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)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index db906f530ec0..ccb369b1af6f 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,14 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+			name = strVal(object);
+			break;
+		case OBJECT_CMK:
+			msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+			name = strVal(object);
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +511,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index a3bdc5db0735..c10732a56ed0 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 9e29584d93ef..f34c5ff25a53 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 845208d662ba..4c80e87c4a4a 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"
@@ -637,6 +638,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);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +938,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);
 
@@ -12696,6 +12709,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:
@@ -19331,3 +19347,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 00d8d54d820a..c67497257b69 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 fd5796f1b9e1..8a7760594509 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 af8620ceb7ce..140a14b7a782 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3982,6 +3982,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 9384214942aa..b218f7d8fdb3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,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
@@ -592,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
@@ -690,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
@@ -714,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
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -3680,14 +3685,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;
@@ -3696,8 +3702,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;
@@ -3754,6 +3760,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -6250,6 +6261,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 +6298,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 +6837,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 +9155,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 +10183,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 +11357,52 @@ 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 COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16771,6 +16890,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16834,6 +16954,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17317,6 +17438,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17405,6 +17527,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 e80876aa25e0..aab47b3cfedb 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,10 +152,24 @@ variable_paramref_hook(ParseState *pstate, ParamRef *pref)
 	{
 		/* Need to enlarge param array */
 		if (*parstate->paramTypes)
+		{
 			*parstate->paramTypes = repalloc0_array(*parstate->paramTypes, Oid,
 													*parstate->numParams, paramno);
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc0_array(*parstate->paramOrigTbls, Oid,
+														   *parstate->numParams, paramno);
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc0_array(*parstate->paramOrigCols, AttrNumber,
+														   *parstate->numParams, paramno);
+		}
 		else
+		{
 			*parstate->paramTypes = palloc0_array(Oid, paramno);
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc0_array(Oid, paramno);
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc0_array(AttrNumber, paramno);
+		}
 		*parstate->numParams = paramno;
 	}
 
@@ -256,6 +277,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 8e0d6fd01f1f..c92f783b5cd9 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 a8a246921f23..24b61d67b17a 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2244,12 +2244,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 3082093d1eab..8b48b1539c5e 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 247d0816ad81..4f1802976a32 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,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,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 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,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 +2665,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 +2791,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 +3100,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3217,7 +3262,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 +3733,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			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 94ca8e12303d..49f73ca4e828 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
  *
@@ -3683,3 +3705,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 cc943205d342..fe2609a97d51 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 474ab476f5fb..33e871ff0608 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 f39c0fa36fdc..971ea4b6c736 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3408,6 +3408,8 @@ _getObjectDescription(PQExpBuffer buf, const 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, "FOREIGN TABLE") == 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 da427f4d4a17..3c7428a60d07 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"
@@ -226,6 +227,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);
@@ -385,6 +388,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},
@@ -677,6 +681,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");
 
@@ -1022,6 +1029,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"
@@ -5518,6 +5526,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
@@ -8108,6 +8248,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;
@@ -8217,17 +8360,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);
@@ -8246,6 +8401,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");
@@ -8307,6 +8465,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));
@@ -8335,6 +8496,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));
@@ -9859,6 +10032,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;
@@ -13252,6 +13431,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
@@ -15335,6 +15661,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)
@@ -17899,6 +18241,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 a87262e33357..695a1cf3079b 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 8dc1f0eccb5d..78d961a8b9c7 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
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 7672ed9e9d56..35bb0845ea10 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 2eae519b1dd8..85c258364a2e 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),
@@ -4476,6 +4499,134 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "cekname", NULL, NULL,
+								NULL, 1))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 3");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "cmkname", NULL, NULL,
+								NULL, 1))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index bd051e09cbbd..debdb60e118b 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index b4e0ec2687fd..1b9de191f6d8 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,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 3fce71b85fe4..4be217909154 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,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 13014f074f40..0c31bbc4728f 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1175,6 +1175,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.
@@ -1208,6 +1218,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
@@ -1690,7 +1702,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1907,6 +1919,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");
@@ -2828,6 +2856,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",
@@ -3553,6 +3601,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 f9301b2627e6..dd19ea87a9d9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8067,9 +8067,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',
@@ -11854,4 +11854,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 0763dfde3942..8651146a7760 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..bfbef9487e84
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *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 f4ed9bbff912..62a3708373a2 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -687,6 +687,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? */
@@ -1866,6 +1867,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2058,6 +2062,31 @@ 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 Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	char	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	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 957ee18d8498..a6084574c583 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/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..8897aa243c9c 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
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index f88d672c6c8a..48b41f9709fe 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}
@@ -2422,6 +2430,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;
@@ -4029,6 +4038,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..0bd3ee1f881d
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,760 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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)
+#define libpq_append_conn_error(conn, ...) appendPQExpBuffer(&(conn)->errorMessage, __VA_ARGS__)
+
+#endif							/* TEST_ENCRYPT */
+
+
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = 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:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								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);
+	BIO_free(bio);
+
+	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;
+	int64		al;
+	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);
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	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)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "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)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								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))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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..c0f9f36250e6
--- /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, const char *cmkfilename, 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 da229d632a1e..9ceecd1f4b93 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,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1187,375 @@ 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, size_t b64datalen)
+{
+	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':
+					appendBinaryPQExpBuffer(&buf, b64data, b64datalen);
+					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, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', 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)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID", 7);
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				int			enclen;
+				char	   *enc;
+				char	   *command;
+				FILE	   *fp;
+				char		buf[4096];
+				int			rc;
+
+				enclen = pg_b64_enc_len(fromlen);
+				enc = malloc(enclen + 1);
+				if (!enc)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				enclen = pg_b64_encode((const char *) from, fromlen, enc, enclen);
+				if (enclen < 0)
+				{
+					libpq_append_conn_error(conn, "base64 encoding failed");
+					free(enc);
+					goto fail;
+				}
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, enc, enclen);
+				free(enc);
+				fp = popen(command, "r");
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					goto fail;
+				}
+				if (fgets(buf, sizeof(buf), fp))
+				{
+					int			linelen;
+					int			declen;
+					char	   *dec;
+
+					linelen = strlen(buf);
+					if (buf[linelen - 1] == '\n')
+					{
+						buf[linelen - 1] = '\0';
+						linelen--;
+					}
+					declen = pg_b64_dec_len(linelen);
+					dec = malloc(declen);
+					if (!dec)
+					{
+						libpq_append_conn_error(conn, "out of memory");
+						free(command);
+						goto fail;
+					}
+					declen = pg_b64_decode(buf, linelen, dec, declen);
+					if (declen < 0)
+					{
+						libpq_append_conn_error(conn, "base64 decoding failed");
+						free(dec);
+						free(command);
+						goto fail;
+					}
+					result = (unsigned char *) dec;
+					*tolen = declen;
+				}
+				else
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					goto fail;
+				}
+				free(command);
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", 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
@@ -1251,6 +1624,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;
@@ -1258,6 +1680,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;
@@ -1500,6 +1923,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1941,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +1980,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2096,19 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2134,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2233,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2282,45 @@ PQsendQueryGuts(PGconn *conn,
 		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;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					format = 1;
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2339,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2357,52 @@ 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)
+				{
+					libpq_append_conn_error(conn, "column encryption key not found");
+					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
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2838,26 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3577,6 +4139,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)
 {
@@ -3762,6 +4335,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 364bad2b882c..280f7dc52d1b 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);
@@ -297,6 +299,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 &&
@@ -547,6 +565,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) ||
@@ -561,6 +581,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.
@@ -582,8 +617,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;
 	}
 
@@ -685,10 +722,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! */
@@ -1468,6 +1526,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.
@@ -2286,6 +2427,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..d4c91dce741f 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,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,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 +559,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 512762f99917..9783ba773698 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);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 8e696f1183cf..a3e2d2e98ba6 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -27,6 +27,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 4df544ecef56..0c36aa5f32b6 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-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_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext pqInternalNotice:2
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 017f729d435d..fea613dd634b 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -34,3 +34,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..76a153e33063
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# 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
+
+export OPENSSL
+
+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..84cfa84e12f8
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,23 @@
+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',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+    },
+  },
+}
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..cda93556270e
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,223 @@
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$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"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	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}, algorithm = '${cmkalg}', 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', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# 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');
+}
+
+TODO: {
+	local $TODO = 'path not being passed correctly on Windows' if $windows_os;
+
+	local $ENV{'PGCMKLOOKUP'} = "*=run:perl ./test_run_decrypt.pl '${PostgreSQL::Test::Utils::tmp_check}' %k %a '%b'";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$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');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$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 update');
+
+
+# Test deterministic encryption
+
+$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', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+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');
+
+
+# Test multiple keys in one table
+
+$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 tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$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 second insert');
+
+
+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..adf63cf69c81
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,110 @@
+# 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 $openssl = $ENV{OPENSSL};
+
+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) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+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..56b715a6b85f
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2021-2022, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	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;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	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 = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 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
+		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..058de7615847
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,60 @@
+#!/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 =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $openssl = $ENV{OPENSSL};
+
+open my $fh, '>:raw', "${tmpdir}/input.tmp" or die $!;
+print $fh decode_base64($b64data);
+close $fh;
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', "${tmpdir}/input.tmp", '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+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..f0fb04048a7a
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,166 @@
+\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 = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+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 SESSION AUTHORIZATION '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 SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 25c174f27503..46ce05d79047 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,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 (
@@ -107,7 +109,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
@@ -327,6 +330,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
@@ -382,6 +403,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
 -- Make sure that NULL handling is correct.
 \pset null 'NULL'
 -- Temporarily disable fancy output, so as future additions never create
@@ -409,6 +438,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}', '{}'),
@@ -505,6 +537,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|NULL|addr_cmk|addr_cmk|t
+column encryption key|NULL|addr_cek|addr_cek|t
+column encryption key data|NULL|NULL|of addr_cek for addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +552,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
@@ -547,6 +584,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
@@ -634,5 +674,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|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/rules.out b/src/test/regress/expected/rules.out
index 37c1c8647394..fbb7a3736c77 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1428,11 +1428,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 9a139f1e2487..775253b50f6c 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..0eebb596d637
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,146 @@
+\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 = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+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 SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d54..9dcc614d9099 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,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 (
@@ -99,7 +101,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
@@ -144,6 +147,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}', '{}');
 
 -- Make sure that NULL handling is correct.
 \pset null 'NULL'
@@ -174,6 +181,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}', '{}'),
@@ -228,6 +238,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;
 
@@ -247,6 +259,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
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/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: cbe6e482d7bf851c6e466697a21dcef7b05cbb59
-- 
2.38.1

#50Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#49)
1 attachment(s)
Re: Transparent column encryption

On 28.11.22 15:05, Peter Eisentraut wrote:

On 23.11.22 19:39, Peter Eisentraut wrote:

Here is another updated patch.  Some preliminary work was committed,
which allowed this patch to get a bit smaller.  I have incorporated
some recent reviews, and also fixed some issues pointed out by recent
CI additions (address sanitizer etc.).

The psql situation in this patch is temporary: It still has the \gencr
command from previous versions, but I plan to fold this into the new
\bind command.

I made a bit of progress with this now, based on recent reviews:

- Cleaned up the libpq API.  PQexecParams() now supports column
encryption transparently.
- psql \bind can be used; \gencr is removed.
- Added psql \dcek and \dcmk commands.
- ALTER COLUMN MASTER KEY to alter realm.

And another update. The main changes are that I added an 'unspecified'
CMK algorithm, which indicates that the external KMS knows what it is
but the database system doesn't. This was discussed a while ago. I
also changed some details about how the "cmklookup" works in libpq.
Also added more code comments and documentation and rearranged some code.

According to my local todo list, this patch is now complete.

Attachments:

v13-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v13-0001-Transparent-column-encryption.patchDownload
From 5f1b7a5eafd02b90370f00744154ff84afbd59a5 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 21 Dec 2022 06:37:11 +0100
Subject: [PATCH v13] 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 asymmetric 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
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.

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).  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.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this
transparently if the \bind command is used.

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.  This
functionality is in principle available to all prepared-statement
variants, not only protocol-level.  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.

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/charset.sgml                     |   9 +
 doc/src/sgml/datatype.sgml                    |  53 ++
 doc/src/sgml/ddl.sgml                         | 372 ++++++++
 doc/src/sgml/glossary.sgml                    |  23 +
 doc/src/sgml/libpq.sgml                       | 314 +++++++
 doc/src/sgml/protocol.sgml                    | 442 +++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 186 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 124 +++
 doc/src/sgml/ref/copy.sgml                    |   9 +
 .../ref/create_column_encryption_key.sgml     | 168 ++++
 .../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                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   7 +
 src/backend/access/common/printtup.c          | 221 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  12 +
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  18 +
 src/backend/catalog/objectaddress.c           | 213 +++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  15 +
 src/backend/commands/colenccmds.c             | 426 +++++++++
 src/backend/commands/dropcmds.c               |   9 +
 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              |  91 ++
 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                     | 148 +++-
 src/backend/parser/parse_param.c              |  35 +-
 src/backend/parser/parse_target.c             |   6 +
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  57 +-
 src/backend/tcop/utility.c                    |  53 ++
 src/backend/utils/adt/arrayfuncs.c            |   1 +
 src/backend/utils/adt/varlena.c               | 106 +++
 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          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 326 ++++++-
 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              |  48 +
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 157 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  51 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 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_colenckey.h            |  40 +
 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               |  39 +-
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  29 +
 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/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             |  25 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 836 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 655 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 139 ++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 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           |  31 +
 src/test/column_encryption/meson.build        |  23 +
 .../t/001_column_encryption.pl                | 238 +++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 164 ++++
 src/test/regress/expected/object_address.out  |  45 +-
 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/rules.out           |   4 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 144 +++
 src/test/regress/sql/object_address.sql       |  17 +-
 src/test/regress/sql/prepare.sql              |   2 +-
 src/test/regress/sql/type_sanity.sql          |   2 +
 139 files changed, 8182 insertions(+), 132 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/common/colenc.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/include/common/colenc.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 2df6559acc..bd1a0185ed 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 9316b811ac..b2b294702c 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>int4</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>
@@ -2467,6 +2516,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>int4</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/charset.sgml b/doc/src/sgml/charset.sgml
index 445fd175d8..ce13af221a 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,15 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When transparent column encryption is used, then no encoding conversion
+     is possible.  (The encoding conversion happens on the server, and the
+     server cannot look inside any encrypted column values.)  If column
+     encryption is enabled for a session, then the server enforces that the
+     client encoding matches the server encoding, and any attempts to change
+     the client encoding will be rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index fdffba4442..985390f14c 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,57 @@ <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.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support transparent column encryption or have disabled
+    it will see the encrypted values in this format.  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 6e92bbddd2..eb35e9c09c 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1211,6 +1211,378 @@ <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 asymmetric 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>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO ssn (id, name, ssn) VALUES ($1, $2, $3)", 3, NULL, values, NULL, NULL, 0);
+
+/* 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>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO ssn (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</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"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</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 on 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 7c01a541fe..a9eb49b15f 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,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 af278660eb..a2d413bafd 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,140 @@ <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>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </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>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </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 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 --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2998,25 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this byte to one to force encryption.
+            For example, use the C code literal <literal>0x10</literal> to
+            specify text format with forced encryption.  If the array pointer
+            is null then encryption is not forced for any parameter.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter 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.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3029,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3188,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</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 *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> 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>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4076,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>
 
@@ -4059,6 +4279,31 @@ <title>Retrieving Query Result Information</title>
      </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-PQexecPreparedDescribed"/> 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>
 
@@ -4584,6 +4829,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4894,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4955,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</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 PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> 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-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5044,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8078,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 03312e07e2..1a9b8abd7f 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,75 @@ <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, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column encryption protocol option is enabled.  That way, a client library
+    only needs to support decrypting data sent in binary and does not have to
+    support decoding the text format of the encryption-related types (see
+    <xref linkend="datatype-encrypted"/>).
+   </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>
 
@@ -4056,6 +4125,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>Int32</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>
@@ -5152,6 +5355,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>Int32</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>
 
@@ -5540,6 +5783,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>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encrypt algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7343,6 +7615,176 @@ <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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</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>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</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>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</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>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</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>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</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 version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </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 IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </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 e90a0e1f83..331b1f010b 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 0000000000..7597cd80ca
--- /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 0000000000..370347ec9c
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,124 @@
+<!--
+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> ( REALM = <replaceable>realm</replaceable> )
+
+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>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </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/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..8c461b8f15 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,15 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support transparent column encryption or
+    decryption; its input or output data will always be the ciphertext.  This
+    is usually suitable for backups (see also <xref linkend="app-pgdump"/>).
+    If transparent encryption or decryption is wanted,
+    <command>INSERT</command> and <command>SELECT</command> need to be used
+    instead to write and read the data.
+   </para>
+
  </refsect1>
 
  <refsect1>
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 0000000000..72e7ce74db
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,168 @@
+<!--
+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>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></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.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </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 0000000000..6a1e3ad360
--- /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 c98223b2a5..e4bf54e2e6 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 | DEFAULT } ] [ 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 | DEFAULT } ] [ 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> ... ] }
     [, ... ]
@@ -351,6 +351,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 0000000000..9d157de9c5
--- /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 0000000000..c85098ea1c
--- /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 2c938cd7e1..5b3cbf919c 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -715,6 +715,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 ... \bind ... \g 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 e62d05e5ab..4bf60c729f 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 8a5285da9a..5f8e44b0bd 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry>
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry>
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry>
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4022,6 +4050,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 a3b743e8c1..e1425c222f 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 c99ae54cb0..1740096a3f 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_sendint32(&buf, 0);	/* CEK alg */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index d2f3b57288..f3ba617fc0 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,156 @@ 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;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	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_sendint32(&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;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	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 +332,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 +349,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(int32));	/* attencalg */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +369,8 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +402,28 @@ 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);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +431,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_writeint32(buf, attencalg);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +469,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 7857f55e24..32b131781f 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attrealtypid != attr2->attrealtypid)
+			return false;
+		if (attr1->attencalg != attr2->attencalg)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 10bf26ce7c..cea91d4a88 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 89a0221ec9..7e1a91b974 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index b5019059e8..fac60d56fb 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.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"
@@ -2798,6 +2800,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:
@@ -2828,6 +2833,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;
@@ -2938,6 +2949,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:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 30394dccf5..3c64763266 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 */
@@ -1493,6 +1499,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 bdd413f01b..c042d92196 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,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_foreign_table.h"
@@ -749,6 +750,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] = Int32GetDatum(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)
@@ -840,6 +844,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attrealtypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attrealtypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index fe97fbf79d..1dfe4b6926 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
@@ -1029,6 +1086,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:
@@ -1108,6 +1167,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));
@@ -1293,6 +1367,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);
@@ -2253,6 +2337,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)
@@ -2327,6 +2412,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:
@@ -2357,6 +2444,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));
@@ -2480,6 +2568,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   NameListToString((castNode(ObjectWithArgs, object))->objname));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_DATABASE:
 		case OBJECT_EVENT_TRIGGER:
 		case OBJECT_EXTENSION:
@@ -2572,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3037,6 +3128,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;
@@ -4461,6 +4594,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);
@@ -4926,6 +5071,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 48f7348f91..69f6175c60 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 10b6fe19a2..db1ada3909 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 0000000000..b4a85101cd
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,426 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, 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 "catalog/pg_database.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.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, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	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")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	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, int 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] = Int32GetDatum(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 = object_aclcheck(DatabaseRelationId, 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;
+		int			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 (!object_ownercheck(ColumnEncKeyRelationId, 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);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			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 = object_aclcheck(DatabaseRelationId, 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;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, stmt->cmkname);
+
+	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)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index db906f530e..ccb369b1af 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,14 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+			name = strVal(object);
+			break;
+		case OBJECT_CMK:
+			msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+			name = strVal(object);
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +511,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index a3bdc5db07..c10732a56e 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 9b350d025f..6e26e158c4 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 9e29584d93..f34c5ff25a 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 7ae19b98bb..07ad646a52 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 56dc995713..9a2c72e1c6 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"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,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);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,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);
 	}
@@ -6830,6 +6836,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);
 
@@ -12661,6 +12675,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:
@@ -19300,3 +19317,77 @@ 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);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				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 00d8d54d82..c67497257b 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 fd5796f1b9..8a77605945 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 af8620ceb7..140a14b7a7 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3982,6 +3982,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 2e593aed2b..f5583c6c20 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 63b4baaed9..be4d2d3029 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,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
@@ -592,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
@@ -690,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
@@ -714,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
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -3700,14 +3705,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;
@@ -3716,8 +3722,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;
@@ -3774,6 +3780,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -6270,6 +6281,33 @@ 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
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (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; }
@@ -6289,6 +6327,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)); }
@@ -6824,6 +6866,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; }
@@ -9140,6 +9184,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);
@@ -10148,6 +10212,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);
@@ -11304,6 +11386,52 @@ 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 COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16919,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16854,6 +16983,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17467,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17425,6 +17556,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 e80876aa25..aab47b3cfe 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,10 +152,24 @@ variable_paramref_hook(ParseState *pstate, ParamRef *pref)
 	{
 		/* Need to enlarge param array */
 		if (*parstate->paramTypes)
+		{
 			*parstate->paramTypes = repalloc0_array(*parstate->paramTypes, Oid,
 													*parstate->numParams, paramno);
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = repalloc0_array(*parstate->paramOrigTbls, Oid,
+														   *parstate->numParams, paramno);
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = repalloc0_array(*parstate->paramOrigCols, AttrNumber,
+														   *parstate->numParams, paramno);
+		}
 		else
+		{
 			*parstate->paramTypes = palloc0_array(Oid, paramno);
+			if (parstate->paramOrigTbls)
+				*parstate->paramOrigTbls = palloc0_array(Oid, paramno);
+			if (parstate->paramOrigCols)
+				*parstate->paramOrigCols = palloc0_array(AttrNumber, paramno);
+		}
 		*parstate->numParams = paramno;
 	}
 
@@ -256,6 +277,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 56d64c8851..8299df48a0 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 f459dab360..d0a4891279 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2244,12 +2244,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 01d264b5ab..61bd05b330 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;
@@ -2615,8 +2634,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_sendint32(&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 247d0816ad..49f818d703 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,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,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 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,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 +2665,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 +2791,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 +3100,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3733,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			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 0d3d46b9a5..1ed83be6d4 100644
--- a/src/backend/utils/adt/arrayfuncs.c
+++ b/src/backend/utils/adt/arrayfuncs.c
@@ -3412,6 +3412,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/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 1c52deec55..0525c787fb 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -663,6 +663,112 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 94ca8e1230..49f73ca4e8 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
  *
@@ -3683,3 +3705,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 cc943205d3..fe2609a97d 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 5f17047047..7dd0896bde 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 24f37e3ec9..05bd0d408d 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 44fa52cc55..89feceb3bd 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 aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,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 7f7a0f1ce7..519d2ad635 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3425,6 +3425,8 @@ _getObjectDescription(PQExpBuffer buf, const 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, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 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 44d957c038..615e2a0028 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,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);
@@ -393,6 +396,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},
@@ -685,6 +689,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");
 
@@ -1057,6 +1064,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"
@@ -5570,6 +5578,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
@@ -8160,6 +8300,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;
@@ -8269,17 +8412,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);
@@ -8298,6 +8453,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");
@@ -8359,6 +8517,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));
@@ -8387,6 +8548,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));
@@ -9911,6 +10084,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;
@@ -13304,6 +13483,129 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * 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_cmkalg_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
@@ -15387,6 +15689,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_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -17951,6 +18269,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 436ac5bb98..0724f3ae08 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 31cee46f3e..bf6446f950 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 7b40081678..a399ef8d58 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 7c3067a3f4..7b3745c018 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -645,6 +645,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, },
@@ -1245,6 +1257,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 $$
@@ -1663,6 +1693,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, algorithm = 'RSAES_OAEP_SHA_1', 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
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index de6a3a71f8..b68467a2df 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -814,7 +814,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index df166365e8..d335ee9a37 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),
@@ -4479,6 +4502,134 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "cekname", NULL, NULL,
+								NULL, 1))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 3");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "cmkname", NULL, NULL,
+								NULL, 1))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index bd051e09cb..debdb60e11 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index b4e0ec2687..1b9de191f6 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,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 3fce71b85f..4be2179091 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,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 f5b9e268f2..0b812f3322 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 2a3921937c..dc8c432450 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1175,6 +1175,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.
@@ -1208,6 +1218,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
@@ -1690,7 +1702,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1907,6 +1919,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");
@@ -2828,6 +2856,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",
@@ -3553,6 +3601,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/common/Makefile b/src/common/Makefile
index e9af7346c9..99d9e39d3d 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		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";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index f69d75e9c6..ed09360d40 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -2,6 +2,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 971a74cf22..db1f3b8811 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,6 +20,8 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 98a1a84289..1a2a8177d1 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 45ffa99692..f3a0b1cee9 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 61cd591430..a0bd708132 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4cc129bebd..dddb27113f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 053294c99f..4741b22814 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) */
+	int32		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_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..d8c605dbc7
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,40 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /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-2023, 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);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..0d248da0a4
--- /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-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_index, 8241, ColumnMasterKeyNameIndexId, on pg_colmasterkey using btree(cmkname name_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index dbcae7ffdd..0ca401ffe4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index bc5f8213f3..4737a7f9ed 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index b3b6a7e616..5343580b06 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 98d90d9338..7a46aab026 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8089,9 +8089,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',
@@ -11876,4 +11876,37 @@
   prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary',
   prosrc => 'brin_minmax_multi_summary_send' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_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 0763dfde39..462adc5b21 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 => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 48a2559137..c1d30903b5 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 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, 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);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml>.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 6d452ec6d9..e32c7779c9 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 34bc640ff2..713789bf1b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -690,6 +690,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? */
@@ -1893,6 +1894,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2080,6 +2084,31 @@ 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 Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	char	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index 3d3a5918c2..6c2866d19f 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 957ee18d84..a6084574c5 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 3fd56ceccd..15d7feefac 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);
 
 
 /*
@@ -227,6 +228,7 @@ struct ParseState
 	PostParseColumnRefHook p_post_columnref_hook;
 	ParseParamRefHook p_paramref_hook;
 	CoerceParamHook p_coerce_param_hook;
+	ParamAssignOrigHook p_param_assign_orig_hook;
 	void	   *p_ref_hook_state;	/* common passthrough link for above */
 };
 
diff --git a/src/include/parser/parse_param.h b/src/include/parser/parse_param.h
index df1ee660d8..da202b28a7 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -18,7 +18,8 @@
 extern void setup_parse_fixed_parameters(ParseState *pstate,
 										 const Oid *paramTypes, int numParams);
 extern void setup_parse_variable_parameters(ParseState *pstate,
-											Oid **paramTypes, int *numParams);
+											Oid **paramTypes, int *numParams,
+											Oid **paramOrigTbls, AttrNumber **paramOrigCols);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
 
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f..d82a2e1171 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 5d34978f32..7502d71b0d 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/lsyscache.h b/src/include/utils/lsyscache.h
index 50f0288305..7258d40077 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 0499635f59..9a7cf794ce 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 4463ea66be..489c6ee734 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 1d31b256fc..4812f862ca 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 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
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index f88d672c6c..48b41f9709 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}
@@ -2422,6 +2430,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;
@@ -4029,6 +4038,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 0000000000..c1112eba83
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,836 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, 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 "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) appendPQExpBuffer(&(conn)->errorMessage, __VA_ARGS__)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = 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;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								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);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05>
+ * for encrypting and decrypting column values.
+ */
+
+#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,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+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;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+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;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+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,
+					 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;
+	int64		al;
+	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;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	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;
+}
+
+/*
+ * Decrypt a column value
+ */
+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			iv_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 = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_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;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_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;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#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)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "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 */
+
+/*
+ * Encrypt a column value
+ */
+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			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_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)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	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, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								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,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf2, encr, encrlen);
+	memcpy(buf2 + encrlen, md_value, md_len);
+
+	result = buf2;
+	nbytes = buf2size;
+
+fail:
+	free(buf);
+	EVP_CIPHER_CTX_free(evp_cipher_ctx);
+
+	*nbytesp = nbytes;
+	return result;
+}
+
+
+/*
+ * Run test cases
+ */
+#ifdef TEST_ENCRYPT
+
+static void
+debug_print_hex(const char *name, const unsigned char *val, int len)
+{
+	printf("%s =", name);
+	for (int i = 0; i < len; i++)
+	{
+		if (i % 16 == 0)
+			printf("\n");
+		else
+			printf(" ");
+		printf("%02x", val[i]);
+	}
+	printf("\n");
+}
+
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdata = unconstify(unsigned char *, K);
+	cek.cekdatalen = K_len;
+
+	C = encrypt_value(NULL, &cek, alg, P, &nbytes, true);
+	debug_print_hex("C", C, nbytes);
+}
+
+int
+main(int argc, char **argv)
+{
+	printf("5.1\n");
+	test_case(PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256, K, 32, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, 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, const char *cmkfilename, 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 da229d632a..ca0cb615cd 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,414 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+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 *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					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, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', 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)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, "r");
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+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
@@ -1251,6 +1662,43 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				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;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				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;
@@ -1258,6 +1706,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;
@@ -1500,6 +1949,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1967,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2006,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2122,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2165,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2264,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2313,47 @@ PQsendQueryGuts(PGconn *conn,
 		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;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2372,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2390,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#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)
+				{
+					libpq_append_conn_error(conn, "column encryption key not found");
+					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
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2872,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4140,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4188,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)
 {
@@ -3762,6 +4384,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 364bad2b88..4bdb45b503 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);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -547,6 +555,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) ||
@@ -561,6 +571,21 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, 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.
@@ -582,8 +607,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;
 	}
 
@@ -685,10 +712,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, 4, 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! */
@@ -1468,6 +1516,92 @@ 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);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	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;
+	int			ret;
+
+	/* 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, 4, 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 */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2420,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 5d68cf2eb3..86b7e64e1f 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);
+			pqTraceOutputInt32(f, message, cursor, false);
+			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);
+			pqTraceOutputInt32(f, message, cursor, false);
+		}
 	}
 }
 
@@ -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);
+	pqTraceOutputInt32(f, message, cursor, false);
+	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 b7df3224c0..d4c91dce74 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,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,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 +559,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 512762f999..9783ba7736 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);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 8e696f1183..a3e2d2e98b 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -27,6 +27,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 4df544ecef..0c36aa5f32 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-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_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext pqInternalNotice:2
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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 017f729d43..fea613dd63 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -34,3 +34,5 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+# TODO: libpq_test_encrypt
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c8ba170503 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 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..764cadf550
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, 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
+
+export OPENSSL
+
+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 0000000000..84cfa84e12
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,23 @@
+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',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..2de31632d0
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,238 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$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"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	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}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, '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', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# 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\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[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 "%p"';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+TODO: {
+	local $TODO = 'path not being passed correctly on Windows' if $windows_os;
+
+	local $ENV{'PGCMKLOOKUP'} = "*=run:perl ./test_run_decrypt.pl '${PostgreSQL::Test::Utils::tmp_check}' %k %a '%p'";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$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\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$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 update');
+
+
+# Test deterministic encryption
+
+$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', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+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');
+
+
+# Test multiple keys in one table
+
+$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 tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$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 second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..bb3b170d3b
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, 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 $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# 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, algorithm = '$cmkalg', 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) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+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, algorithm = '$cmkalg', 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 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	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;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	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 = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	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
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..94bb69066a
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/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-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+use MIME::Base64;
+
+my ($tmpdir, $cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $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}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 241d9d48aa..0d39cedeb1 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 0000000000..a5b691e32a
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,164 @@
+\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;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+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 SESSION AUTHORIZATION '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 SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 25c174f275..fe045dff4c 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -107,7 +109,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
@@ -327,6 +330,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
@@ -382,6 +403,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
 -- Make sure that NULL handling is correct.
 \pset null 'NULL'
 -- Temporarily disable fancy output, so as future additions never create
@@ -409,6 +438,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}', '{}'),
@@ -505,6 +537,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|NULL|addr_cmk|addr_cmk|t
+column encryption key|NULL|addr_cek|addr_cek|t
+column encryption key data|NULL|NULL|of addr_cek for addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +552,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
@@ -547,6 +584,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
@@ -634,5 +674,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|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 215eb899be..2aa0e16323 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attrealtypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,7 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ 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)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,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
@@ -872,6 +876,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
diff --git a/src/test/regress/expected/prepare.out b/src/test/regress/expected/prepare.out
index 5815e17b39..4482a65d24 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/rules.out b/src/test/regress/expected/rules.out
index fb9f936d43..f123f3f31e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1428,11 +1428,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 a640cfc476..2e47131b37 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,
@@ -676,6 +678,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  'encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98'::pg_encrypted_det,
+  'encrypted$3dade6cec75b107d379f397876c70640e1a1f39bd20884339bc64203abe73d6d'::pg_encrypted_rnd,
   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 9a139f1e24..08a00e1dd4 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -106,7 +106,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index a4b354c9e6..8ad1f458d5 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..5010ca82c3
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,144 @@
+\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;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+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 SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..25a7f2ae98 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -99,7 +101,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
@@ -144,6 +147,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}', '{}');
 
 -- Make sure that NULL handling is correct.
 \pset null 'NULL'
@@ -174,6 +181,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}', '{}'),
@@ -228,6 +238,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;
 
@@ -247,6 +259,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
diff --git a/src/test/regress/sql/prepare.sql b/src/test/regress/sql/prepare.sql
index c6098dc95c..7db788735c 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/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..4c4cf39398 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -503,6 +503,8 @@ CREATE TABLE tab_core_types AS SELECT
   'txt'::text,
   true::bool,
   E'\\xDEADBEEF'::bytea,
+  'encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98'::pg_encrypted_det,
+  'encrypted$3dade6cec75b107d379f397876c70640e1a1f39bd20884339bc64203abe73d6d'::pg_encrypted_rnd,
   B'10001'::bit,
   B'10001'::varbit AS varbit,
   '12.34'::money,

base-commit: cca186348929cd75f23ef1b25922386bf38cf99c
-- 
2.39.0

#51Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#50)
1 attachment(s)
Re: Transparent column encryption

On 21.12.22 06:46, Peter Eisentraut wrote:

And another update.  The main changes are that I added an 'unspecified'
CMK algorithm, which indicates that the external KMS knows what it is
but the database system doesn't.  This was discussed a while ago.  I
also changed some details about how the "cmklookup" works in libpq. Also
added more code comments and documentation and rearranged some code.

According to my local todo list, this patch is now complete.

Another update, with some merge conflicts resolved. I also fixed up the
remaining TODO markers in the code, which had something to do with Perl
and Windows. I did some more work on schema handling, e.g., CREATE
TABLE / LIKE, views, partitioning etc. on top of encrypted columns,
mostly tedious and repetitive, nothing interesting. I also rewrote the
code that extracts the underlying tables and columns corresponding to
query parameters. It's now much simpler and better encapsulated.

Attachments:

v14-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v14-0001-Transparent-column-encryption.patchDownload
From 1e2cabc357ca2ec9d515dd12e8337b647bb1bc65 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Sat, 31 Dec 2022 14:55:58 +0100
Subject: [PATCH v14] 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 asymmetric 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
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.

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).  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.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this
transparently if the \bind command is used.

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.

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/charset.sgml                     |   9 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 391 ++++++++
 doc/src/sgml/glossary.sgml                    |  23 +
 doc/src/sgml/libpq.sgml                       | 314 +++++++
 doc/src/sgml/protocol.sgml                    | 442 +++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 186 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 124 +++
 doc/src/sgml/ref/copy.sgml                    |   9 +
 .../ref/create_column_encryption_key.sgml     | 168 ++++
 .../sgml/ref/create_column_master_key.sgml    | 106 +++
 doc/src/sgml/ref/create_table.sgml            |  54 +-
 .../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                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   7 +
 src/backend/access/common/printtup.c          | 221 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  12 +
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/objectaddress.c           | 213 +++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  15 +
 src/backend/commands/colenccmds.c             | 426 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/dropcmds.c               |   9 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 116 +++
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 149 +++-
 src/backend/parser/parse_param.c              | 144 +++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  53 ++
 src/backend/utils/adt/varlena.c               | 106 +++
 src/backend/utils/cache/lsyscache.c           | 111 +++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  40 +
 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          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 326 ++++++-
 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              |  48 +
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 160 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  51 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 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_colenckey.h            |  40 +
 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               |  33 +
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  40 +-
 src/include/parser/kwlist.h                   |   2 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   6 +
 src/include/utils/lsyscache.h                 |   6 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  25 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 842 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 655 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 139 ++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  24 +
 .../t/001_column_encryption.pl                | 255 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 231 +++++
 src/test/regress/expected/object_address.out  |  45 +-
 src/test/regress/expected/oidjoins.out        |   6 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 171 ++++
 src/test/regress/sql/object_address.sql       |  17 +-
 src/test/regress/sql/type_sanity.sql          |   2 +
 134 files changed, 8536 insertions(+), 72 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/common/colenc.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/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 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 2df6559acc..bd1a0185ed 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 9316b811ac..b2b294702c 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>int4</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>
@@ -2467,6 +2516,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>int4</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/charset.sgml b/doc/src/sgml/charset.sgml
index 445fd175d8..ce13af221a 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,15 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When transparent column encryption is used, then no encoding conversion
+     is possible.  (The encoding conversion happens on the server, and the
+     server cannot look inside any encrypted column values.)  If column
+     encryption is enabled for a session, then the server enforces that the
+     client encoding matches the server encoding, and any attempts to change
+     the client encoding will be rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index fdffba4442..56b2e1d0d1 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <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.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support transparent column encryption or have disabled
+    it will see the encrypted values in this format.  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 6e92bbddd2..55f33a2f5f 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1211,6 +1211,397 @@ <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 asymmetric 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>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)", 3, NULL, values, NULL, NULL, 0);
+
+/* 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>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</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"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</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>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation).
+   </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 on 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 7c01a541fe..a9eb49b15f 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,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 af278660eb..a2d413bafd 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,140 @@ <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>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </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>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </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 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 --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2998,25 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this byte to one to force encryption.
+            For example, use the C code literal <literal>0x10</literal> to
+            specify text format with forced encryption.  If the array pointer
+            is null then encryption is not forced for any parameter.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter 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.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3029,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3188,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</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 *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> 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>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4076,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>
 
@@ -4059,6 +4279,31 @@ <title>Retrieving Query Result Information</title>
      </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-PQexecPreparedDescribed"/> 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>
 
@@ -4584,6 +4829,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4894,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4955,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</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 PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> 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-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5044,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8078,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 03312e07e2..1a9b8abd7f 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,75 @@ <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, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column encryption protocol option is enabled.  That way, a client library
+    only needs to support decrypting data sent in binary and does not have to
+    support decoding the text format of the encryption-related types (see
+    <xref linkend="datatype-encrypted"/>).
+   </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>
 
@@ -4056,6 +4125,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>Int32</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>
@@ -5152,6 +5355,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>Int32</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>
 
@@ -5540,6 +5783,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>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encrypt algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7343,6 +7615,176 @@ <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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</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>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</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>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</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>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</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>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</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 version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </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 IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </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 e90a0e1f83..331b1f010b 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 0000000000..7597cd80ca
--- /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 0000000000..370347ec9c
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,124 @@
+<!--
+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> ( REALM = <replaceable>realm</replaceable> )
+
+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>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </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/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..8c461b8f15 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,15 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support transparent column encryption or
+    decryption; its input or output data will always be the ciphertext.  This
+    is usually suitable for backups (see also <xref linkend="app-pgdump"/>).
+    If transparent encryption or decryption is wanted,
+    <command>INSERT</command> and <command>SELECT</command> need to be used
+    instead to write and read the data.
+   </para>
+
  </refsect1>
 
  <refsect1>
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 0000000000..72e7ce74db
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,168 @@
+<!--
+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>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></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.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </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 0000000000..6a1e3ad360
--- /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 c98223b2a5..c167b181ff 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 | DEFAULT } ] [ 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 | DEFAULT } ] [ 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> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,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>
@@ -704,6 +744,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry>
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry>
         <term><literal>INCLUDING GENERATED</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 0000000000..9d157de9c5
--- /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 0000000000..c85098ea1c
--- /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 2c938cd7e1..5b3cbf919c 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -715,6 +715,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 ... \bind ... \g 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 e62d05e5ab..4bf60c729f 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 8a5285da9a..5f8e44b0bd 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry>
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry>
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry>
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4022,6 +4050,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 a3b743e8c1..e1425c222f 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 c99ae54cb0..1740096a3f 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_sendint32(&buf, 0);	/* CEK alg */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index d2f3b57288..f3ba617fc0 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,156 @@ 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;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	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_sendint32(&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;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	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 +332,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 +349,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(int32));	/* attencalg */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +369,8 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +402,28 @@ 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);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +431,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_writeint32(buf, attencalg);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +469,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 7857f55e24..32b131781f 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -459,6 +459,12 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attislocal != attr2->attislocal)
 			return false;
+		if (attr1->attcek != attr2->attcek)
+			return false;
+		if (attr1->attrealtypid != attr2->attrealtypid)
+			return false;
+		if (attr1->attencalg != attr2->attencalg)
+			return false;
 		if (attr1->attinhcount != attr2->attinhcount)
 			return false;
 		if (attr1->attcollation != attr2->attcollation)
@@ -629,6 +635,9 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
@@ -690,6 +699,9 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attgenerated = '\0';
 	att->attisdropped = false;
 	att->attislocal = true;
+	att->attcek = 0;
+	att->attrealtypid = 0;
+	att->attencalg = 0;
 	att->attinhcount = 0;
 	/* variable-length fields are not present in tupledescs */
 
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 10bf26ce7c..cea91d4a88 100644
--- a/src/backend/access/hash/hashvalidate.c
+++ b/src/backend/access/hash/hashvalidate.c
@@ -331,7 +331,7 @@ check_hash_func_signature(Oid funcid, int16 amprocnum, Oid argtype)
 				 argtype == BOOLOID)
 			 /* okay, allowed use of hashchar() */ ;
 		else if ((funcid == F_HASHVARLENA || funcid == F_HASHVARLENAEXTENDED) &&
-				 argtype == BYTEAOID)
+				 (argtype == BYTEAOID || argtype == PG_ENCRYPTED_DETOID))
 			 /* okay, allowed use of hashvarlena() */ ;
 		else
 			result = false;
diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile
index 89a0221ec9..7e1a91b974 100644
--- a/src/backend/catalog/Makefile
+++ b/src/backend/catalog/Makefile
@@ -72,7 +72,8 @@ CATALOG_HEADERS := \
 	pg_collation.h pg_parameter_acl.h pg_partitioned_table.h \
 	pg_range.h pg_transform.h \
 	pg_sequence.h pg_publication.h pg_publication_namespace.h \
-	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h
+	pg_publication_rel.h pg_subscription.h pg_subscription_rel.h \
+	pg_colmasterkey.h pg_colenckey.h pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h) schemapg.h system_fk_info.h
 
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index cdfd637815..00dea05588 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.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"
@@ -2798,6 +2800,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:
@@ -2828,6 +2833,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;
@@ -2938,6 +2949,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:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 30394dccf5..3c64763266 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 */
@@ -1493,6 +1499,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 bdd413f01b..abdcbf13bb 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,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_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,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] = Int32GetDatum(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)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attrealtypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attrealtypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index a1df8b1ddc..4ccfc71cfd 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
@@ -1029,6 +1086,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:
@@ -1108,6 +1167,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));
@@ -1293,6 +1367,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));
@@ -2475,6 +2563,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   NameListToString((castNode(ObjectWithArgs, object))->objname));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_DATABASE:
 		case OBJECT_EVENT_TRIGGER:
 		case OBJECT_EXTENSION:
@@ -2567,6 +2657,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3032,6 +3123,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;
@@ -4432,6 +4565,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);
@@ -4897,6 +5042,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 48f7348f91..69f6175c60 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 10b6fe19a2..db1ada3909 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 0000000000..b4a85101cd
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,426 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, 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 "catalog/pg_database.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.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, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	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")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	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, int 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] = Int32GetDatum(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 = object_aclcheck(DatabaseRelationId, 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;
+		int			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 (!object_ownercheck(ColumnEncKeyRelationId, 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);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			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 = object_aclcheck(DatabaseRelationId, 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;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, stmt->cmkname);
+
+	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)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 152c29b551..bf13066495 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attrealtypid,
+													orig_att->atttypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index db906f530e..ccb369b1af 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,14 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+			name = strVal(object);
+			break;
+		case OBJECT_CMK:
+			msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+			name = strVal(object);
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +511,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index a3bdc5db07..c10732a56e 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 867911c0d3..81f1942004 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ae19b98bb..07ad646a52 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 4bea7b3c90..f8e8141e49 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"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,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);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,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);
 	}
@@ -2637,6 +2643,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attrealtypid,
+														attribute->atttypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -6830,6 +6842,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);
 
@@ -12661,6 +12681,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:
@@ -19300,3 +19323,96 @@ 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);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				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;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	return list_make3(makeDefElem("column_encryption_key",
+								  (Node *) makeTypeName(get_cek_name(attr->attcek, false)),
+								  -1),
+					  makeDefElem("encryption_type",
+								  (Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														"deterministic" : "randomized"),
+								  -1),
+					  makeDefElem("algorithm",
+								  (Node *) makeString(pstrdup(get_cekalg_name(attr->attencalg))),
+								  -1));
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index 00d8d54d82..c67497257b 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/commands/view.c b/src/backend/commands/view.c
index 8e3c1efae4..81063413cf 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attrealtypid,
+													orig_att->atttypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index af8620ceb7..140a14b7a7 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3982,6 +3982,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/gram.y b/src/backend/parser/gram.y
index 63b4baaed9..df5068a0d8 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,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
@@ -592,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
@@ -690,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
@@ -714,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
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -3700,14 +3705,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;
@@ -3716,8 +3722,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;
@@ -3774,6 +3780,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4045,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6282,33 @@ 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
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (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; }
@@ -6289,6 +6328,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)); }
@@ -6824,6 +6867,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; }
@@ -9140,6 +9185,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);
@@ -10148,6 +10213,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);
@@ -11304,6 +11387,52 @@ 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 COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16920,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16854,6 +16984,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17468,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17425,6 +17557,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 e80876aa25..f1bfd5634e 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,146 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(te->expr, Param))
+			{
+				Param	   *p = (Param *) te->expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(te->expr, Var))
+			{
+				Var	   *v = (Var *) te->expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell *lc;
+
+					foreach (lc, srcrte->values_lists)
+					{
+						List *values_list = lfirst_node(List, lc);
+						Node *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach (lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f743cd548c..098af680e9 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1040,8 +1040,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attrealtypid,
+												attribute->atttypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index f459dab360..d0a4891279 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2244,12 +2244,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 01d264b5ab..0245aff7bf 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -45,6 +45,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -72,6 +73,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"
 
@@ -1813,6 +1815,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 %d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2554,6 +2566,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2612,11 +2626,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	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 = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				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 + 1)));
+
+			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_sendint32(&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 247d0816ad..49f818d703 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,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,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 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,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 +2665,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 +2791,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 +3100,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3733,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 1c52deec55..0525c787fb 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -663,6 +663,112 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 94ca8e1230..49f73ca4e8 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
  *
@@ -3683,3 +3705,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 cc943205d3..9f66c32bfe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index d3add33527..316e7b82ed 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"
@@ -219,6 +222,31 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAME] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameIndexId,
+		KEY(Anum_pg_colenckey_cekname),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +261,18 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAME] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 24f37e3ec9..05bd0d408d 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 44fa52cc55..89feceb3bd 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 aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,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 7f7a0f1ce7..519d2ad635 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3425,6 +3425,8 @@ _getObjectDescription(PQExpBuffer buf, const 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, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 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 44d957c038..615e2a0028 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,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);
@@ -393,6 +396,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},
@@ -685,6 +689,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");
 
@@ -1057,6 +1064,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"
@@ -5570,6 +5578,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
@@ -8160,6 +8300,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;
@@ -8269,17 +8412,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);
@@ -8298,6 +8453,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");
@@ -8359,6 +8517,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));
@@ -8387,6 +8548,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));
@@ -9911,6 +10084,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;
@@ -13304,6 +13483,129 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * 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_cmkalg_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
@@ -15387,6 +15689,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_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -17951,6 +18269,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 436ac5bb98..0724f3ae08 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 31cee46f3e..bf6446f950 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 7b40081678..a399ef8d58 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 7c3067a3f4..7b3745c018 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -645,6 +645,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, },
@@ -1245,6 +1257,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 $$
@@ -1663,6 +1693,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, algorithm = 'RSAES_OAEP_SHA_1', 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
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index de6a3a71f8..b68467a2df 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -814,7 +814,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index df166365e8..55b90adf33 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,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			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 +2047,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 +2141,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),
@@ -4479,6 +4505,134 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "cekname", NULL, NULL,
+								NULL, 1))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 3");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								NULL, "cmkname", NULL, NULL,
+								NULL, 1))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index bd051e09cb..debdb60e11 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index b4e0ec2687..1b9de191f6 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,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 3fce71b85f..4be2179091 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,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 f5b9e268f2..0b812f3322 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 2a3921937c..dc8c432450 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1175,6 +1175,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.
@@ -1208,6 +1218,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
@@ -1690,7 +1702,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1907,6 +1919,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");
@@ -2828,6 +2856,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",
@@ -3553,6 +3601,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/common/Makefile b/src/common/Makefile
index 898701fae1..67e94d7506 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		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";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index a92dfb9f4a..5005d88175 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -4,6 +4,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 971a74cf22..db1f3b8811 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,6 +20,8 @@ extern DestReceiver *printtup_create_DR(CommandDest dest);
 
 extern void SetRemoteDestReceiverParams(DestReceiver *self, Portal portal);
 
+extern void MaybeSendColumnEncryptionKeyMessage(Oid attcek);
+
 extern void SendRowDescriptionMessage(StringInfo buf,
 									  TupleDesc typeinfo, List *targetlist, int16 *formats);
 
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 98a1a84289..1a2a8177d1 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/heap.h b/src/include/catalog/heap.h
index 5774c46471..c005deeb68 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 04a9a0978a..2dd1b62f8a 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,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 61cd591430..a0bd708132 100644
--- a/src/include/catalog/pg_amop.dat
+++ b/src/include/catalog/pg_amop.dat
@@ -1028,6 +1028,11 @@
   amoprighttype => 'bytea', amopstrategy => '1', amopopr => '=(bytea,bytea)',
   amopmethod => 'hash' },
 
+# pg_encrypted_det_ops
+{ amopfamily => 'hash/pg_encrypted_det_ops', amoplefttype => 'pg_encrypted_det',
+  amoprighttype => 'pg_encrypted_det', amopstrategy => '1', amopopr => '=(pg_encrypted_det,pg_encrypted_det)',
+  amopmethod => 'hash' },
+
 # xid_ops
 { amopfamily => 'hash/xid_ops', amoplefttype => 'xid', amoprighttype => 'xid',
   amopstrategy => '1', amopopr => '=(xid,xid)', amopmethod => 'hash' },
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4cc129bebd..dddb27113f 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -402,6 +402,11 @@
 { amprocfamily => 'hash/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '2',
   amproc => 'hashvarlenaextended' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '1', amproc => 'hashvarlena' },
+{ amprocfamily => 'hash/pg_encrypted_det_ops', amproclefttype => 'pg_encrypted_det',
+  amprocrighttype => 'pg_encrypted_det', amprocnum => '2',
+  amproc => 'hashvarlenaextended' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
   amprocrighttype => 'xid', amprocnum => '1', amproc => 'hashint4' },
 { amprocfamily => 'hash/xid_ops', amproclefttype => 'xid',
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index 053294c99f..4741b22814 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) */
+	int32		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_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..d8c605dbc7
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,40 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /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-2023, 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);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..0d248da0a4
--- /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-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/pg_colmasterkey.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_COLMASTERKEY_H
+#define PG_COLMASTERKEY_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_colmasterkey_d.h"
+
+/* ----------------
+ *		pg_colmasterkey definition. cpp turns this into
+ *		typedef struct FormData_pg_colmasterkey
+ * ----------------
+ */
+CATALOG(pg_colmasterkey,8233,ColumnMasterKeyRelationId)
+{
+	Oid			oid;
+	NameData	cmkname;
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colmasterkey;
+
+typedef FormData_pg_colmasterkey *Form_pg_colmasterkey;
+
+DECLARE_TOAST(pg_colmasterkey, 8235, 8236);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colmasterkey_oid_index, 8239, ColumnMasterKeyOidIndexId, on pg_colmasterkey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_index, 8241, ColumnMasterKeyNameIndexId, on pg_colmasterkey using btree(cmkname name_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index dbcae7ffdd..0ca401ffe4 100644
--- a/src/include/catalog/pg_opclass.dat
+++ b/src/include/catalog/pg_opclass.dat
@@ -166,6 +166,8 @@
   opcintype => 'bool' },
 { opcmethod => 'hash', opcname => 'bytea_ops', opcfamily => 'hash/bytea_ops',
   opcintype => 'bytea' },
+{ opcmethod => 'hash', opcname => 'pg_encrypted_det_ops', opcfamily => 'hash/pg_encrypted_det_ops',
+  opcintype => 'pg_encrypted_det' },
 { opcmethod => 'btree', opcname => 'tid_ops', opcfamily => 'btree/tid_ops',
   opcintype => 'tid' },
 { opcmethod => 'hash', opcname => 'xid_ops', opcfamily => 'hash/xid_ops',
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index bc5f8213f3..4737a7f9ed 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,14 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', oprcanmerge => 'f', oprcanhash => 't', oprleft => 'pg_encrypted_det',
+  oprright => 'pg_encrypted_det', oprresult => 'bool', oprcom => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprnegate => '<>(pg_encrypted_det,pg_encrypted_det)', oprcode => 'pg_encrypted_det_eq', oprrest => 'eqsel',
+  oprjoin => 'eqjoinsel' },
+{ oid => '8248', descr => 'not equal',
+  oprname => '<>', oprleft => 'pg_encrypted_det', oprright => 'pg_encrypted_det', oprresult => 'bool',
+  oprcom => '<>(pg_encrypted_det,pg_encrypted_det)', oprnegate => '=(pg_encrypted_det,pg_encrypted_det)',
+  oprcode => 'pg_encrypted_det_ne', oprrest => 'neqsel', oprjoin => 'neqjoinsel' },
+
 ]
diff --git a/src/include/catalog/pg_opfamily.dat b/src/include/catalog/pg_opfamily.dat
index b3b6a7e616..5343580b06 100644
--- a/src/include/catalog/pg_opfamily.dat
+++ b/src/include/catalog/pg_opfamily.dat
@@ -108,6 +108,8 @@
   opfmethod => 'hash', opfname => 'bool_ops' },
 { oid => '2223',
   opfmethod => 'hash', opfname => 'bytea_ops' },
+{ oid => '8249',
+  opfmethod => 'hash', opfname => 'pg_encrypted_det_ops' },
 { oid => '2789',
   opfmethod => 'btree', opfname => 'tid_ops' },
 { oid => '2225',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7056c95371..2c03679926 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -11887,4 +11887,37 @@
   prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary',
   prosrc => 'brin_minmax_multi_summary_send' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_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 0763dfde39..462adc5b21 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 => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 48a2559137..c1d30903b5 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 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, 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);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 07eac9a26c..ca8541d613 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -104,4 +104,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml>.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 6d452ec6d9..e32c7779c9 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 34bc640ff2..2f71b9b2ab 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -690,6 +690,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? */
@@ -726,11 +727,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1893,6 +1895,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2080,6 +2085,31 @@ 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 Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	char	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 957ee18d84..a6084574c5 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_param.h b/src/include/parser/parse_param.h
index df1ee660d8..d1e661d55a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 9e94f44c5f..d82a2e1171 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/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 50f0288305..7258d40077 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 0499635f59..686a911ac5 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index 4463ea66be..489c6ee734 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 1d31b256fc..4812f862ca 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 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
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index f88d672c6c..48b41f9709 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}
@@ -2422,6 +2430,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;
@@ -4029,6 +4038,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 0000000000..ab6c70967f
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,842 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, 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 "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = 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;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								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);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05>
+ * for encrypting and decrypting column values.
+ */
+
+#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,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+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;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+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;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+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,
+					 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;
+	int64		al;
+	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;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	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;
+}
+
+/*
+ * Decrypt a column value
+ */
+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			iv_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 = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_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;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_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;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#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)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "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 */
+
+/*
+ * Encrypt a column value
+ */
+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			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_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)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	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, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								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,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, 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, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, 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, const char *cmkfilename, 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 da229d632a..b11fe472ff 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,414 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+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 *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					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, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', 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)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, "r");
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+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
@@ -1251,6 +1662,43 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				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;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				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;
@@ -1258,6 +1706,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;
@@ -1500,6 +1949,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1967,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2006,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2122,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2165,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2264,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2313,47 @@ PQsendQueryGuts(PGconn *conn,
 		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;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2372,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2390,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#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)
+				{
+					libpq_append_conn_error(conn, "column encryption key not found");
+					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
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2872,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4140,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4188,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)
 {
@@ -3762,6 +4384,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 364bad2b88..4bdb45b503 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);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -547,6 +555,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) ||
@@ -561,6 +571,21 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, 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.
@@ -582,8 +607,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;
 	}
 
@@ -685,10 +712,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, 4, 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! */
@@ -1468,6 +1516,92 @@ 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);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	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;
+	int			ret;
+
+	/* 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, 4, 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 */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2420,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 5d68cf2eb3..86b7e64e1f 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);
+			pqTraceOutputInt32(f, message, cursor, false);
+			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);
+			pqTraceOutputInt32(f, message, cursor, false);
+		}
 	}
 }
 
@@ -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);
+	pqTraceOutputInt32(f, message, cursor, false);
+	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 b7df3224c0..d4c91dce74 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,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,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 +559,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 512762f999..9783ba7736 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);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index e56109dd58..2be06dffc3 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,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
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': get_option('ssl')},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 4df544ecef..0c36aa5f32 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-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_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext pqInternalNotice:2
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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 ddecfd4fc4..d0ff0ebf80 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c8ba170503 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 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..d5ead874e5
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, 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
+
+export OPENSSL PERL
+
+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 0000000000..47f88c41ce
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,24 @@
+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',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+      'PERL': perl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..1e204605ce
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,255 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+my $perl = $ENV{PERL};
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$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"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	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}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, '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', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# 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\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[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 %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = "*=run:$perl ./test_run_decrypt.pl %k %a %p";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$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\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$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 update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$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', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+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');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$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 tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$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 second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..1a012ccd21
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, 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 $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# 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, algorithm = '$cmkalg', 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) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+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, algorithm = '$cmkalg', 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 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	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;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	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 = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	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
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/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-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $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}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index f16e00a8a0..0b4fbb25ab 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,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 0000000000..b16e035165
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,231 @@
+\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;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+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       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT tbl_29f3.a,
+    tbl_29f3.b,
+    tbl_29f3.c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+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 SESSION AUTHORIZATION '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 SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 25c174f275..fe045dff4c 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -107,7 +109,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
@@ -327,6 +330,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
@@ -382,6 +403,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
 -- Make sure that NULL handling is correct.
 \pset null 'NULL'
 -- Temporarily disable fancy output, so as future additions never create
@@ -409,6 +438,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}', '{}'),
@@ -505,6 +537,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|NULL|addr_cmk|addr_cmk|t
+column encryption key|NULL|addr_cek|addr_cek|t
+column encryption key data|NULL|NULL|of addr_cek for addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +552,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
@@ -547,6 +584,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
@@ -634,5 +674,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|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 215eb899be..2aa0e16323 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -73,6 +73,8 @@ NOTICE:  checking pg_type {typbasetype} => pg_type {oid}
 NOTICE:  checking pg_type {typcollation} => pg_collation {oid}
 NOTICE:  checking pg_attribute {attrelid} => pg_class {oid}
 NOTICE:  checking pg_attribute {atttypid} => pg_type {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attrealtypid} => pg_type {oid}
 NOTICE:  checking pg_attribute {attcollation} => pg_collation {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
@@ -266,3 +268,7 @@ NOTICE:  checking pg_subscription {subdbid} => pg_database {oid}
 NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {cekowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckeydata {ckdcekid} => pg_colenckey {oid}
+NOTICE:  checking pg_colenckeydata {ckdcmkid} => pg_colmasterkey {oid}
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ 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)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,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
@@ -872,6 +876,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
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 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,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 9a139f1e24..08a00e1dd4 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -106,7 +106,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index a4b354c9e6..8ad1f458d5 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..5090cdaecb
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,171 @@
+\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;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+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
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+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 SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..25a7f2ae98 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -99,7 +101,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
@@ -144,6 +147,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}', '{}');
 
 -- Make sure that NULL handling is correct.
 \pset null 'NULL'
@@ -174,6 +181,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}', '{}'),
@@ -228,6 +238,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;
 
@@ -247,6 +259,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
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 8ad51b5f446b5c19ba2c0033a0f7b3180b3b6d95
-- 
2.39.0

#52Justin Pryzby
pryzby@telsasoft.com
In reply to: Peter Eisentraut (#50)
1 attachment(s)
Re: Transparent column encryption

"ON (CASE WHEN a.attrealtypid <> 0 THEN a.attrealtypid ELSE a.atttypid END = t.oid)\n"

This breaks interoperability with older servers:
ERROR: column a.attrealtypid does not exist

Same in describe.c

Find attached some typos and bad indentation. I'm sending this off now
as I've already sat on it for 2 weeks since starting to look at the
patch.

--
Justin

Attachments:

0001-f.txttext/x-diff; charset=us-asciiDownload
From d9dcf23d25ba4452fb12c4b065ab5215e2228882 Mon Sep 17 00:00:00 2001
From: Justin Pryzby <pryzbyj@telsasoft.com>
Date: Fri, 6 Jan 2023 18:28:59 -0600
Subject: [PATCH] f!

---
 doc/src/sgml/datatype.sgml     |  4 ++--
 doc/src/sgml/ddl.sgml          |  4 ++--
 doc/src/sgml/libpq.sgml        |  2 +-
 doc/src/sgml/protocol.sgml     |  2 +-
 src/bin/pg_dump/pg_dump.c      |  4 ++--
 src/interfaces/libpq/fe-exec.c | 12 ++++++------
 6 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 56b2e1d0d1e..243e6861506 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5369,7 +5369,7 @@ WHERE ...
     <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
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
     an equals operator that allows lookup of encrypted values.  It is,
     however, not allowed to create a table using one of these types directly
     as a column type.
@@ -5383,7 +5383,7 @@ WHERE ...
     Clients that don't support transparent column encryption or have disabled
     it will see the encrypted values in this format.  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 protocol layer will translate them back to the declared underlying type in
     the table definition.
    </para>
 
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index b8364a91f9a..a7624d6a60c 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1263,7 +1263,7 @@ CREATE TABLE customers (
    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
+   This prevents someone with direct access to the database server from making
    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
@@ -1540,7 +1540,7 @@ export PGCMKLOOKUP
 
    <para>
     In general, column encryption is never a replacement for additional
-    security and encryption techniques such as transmission encryption
+    security and encryption techniques such as transport 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.
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index a2d413bafd5..9b7f76db7a9 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -3012,7 +3012,7 @@ PGresult *PQexecParams(PGconn *conn,
             If encryption is forced for a parameter but the parameter 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
+            for additional security against a compromised 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.)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 1a9b8abd7f2..caa6e3174ee 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -5807,7 +5807,7 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
        <listitem>
         <para>
          If the field is encrypted, this specifies the identifier of the
-         encrypt algorithm, else zero.
+         encryption algorithm, else zero.
         </para>
        </listitem>
       </varlistentry>
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index f82c2496fd5..99f2583c34a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -690,7 +690,7 @@ main(int argc, char **argv)
 	 * --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");
+		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");
@@ -13546,7 +13546,7 @@ dumpColumnEncryptionKey(Archive *fout, const CekInfo *cekinfo)
 
 		appendPQExpBuffer(query, ")");
 		if (i < cekinfo->numdata - 1)
-		appendPQExpBuffer(query, ", ");
+			appendPQExpBuffer(query, ", ");
 	}
 
 	appendPQExpBufferStr(query, ";\n");
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 4e835d6c681..7c7e2bac128 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -1699,13 +1699,13 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			}
 			else
 			{
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
-				goto fail;
+				val = (char *) pqResultAlloc(res, clen + 1, isbinary);
+				if (val == NULL)
+					goto fail;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* copy and zero-terminate the data (even if it's binary) */
+				memcpy(val, columns[i].value, clen);
+				val[clen] = '\0';
 			}
 
 			tup[i].len = clen;
-- 
2.25.1

#53Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Peter Eisentraut (#51)
Re: Transparent column encryption

On Dec 31, 2022, at 6:17 AM, Peter Eisentraut <peter.eisentraut@enterprisedb.com> wrote:

Another update, with some merge conflicts resolved.

Hi Peter, thanks for the patch!

I wonder if logical replication could be made to work more easily with this feature. Specifically, subscribers of encrypted columns will need the encrypted column encryption key (CEK) and the name of the column master key (CMD) as exists on the publisher, but getting access to that is not automated as far as I can see. It doesn't come through automatically as part of a subscription, and publisher's can't publish the pg_catalog tables where the keys are kept (because publishing system tables is not supported.) Is it reasonable to make available the CEK and CMK to subscribers in an automated fashion, to facilitate setting up logical replication with less manual distribution of key information? Is this already done, and I'm just not recognizing that you've done it?

Can we do anything about the attack vector wherein a malicious DBA simply copies the encrypted datum from one row to another? Imagine the DBA Alice wants to murder a hospital patient Bob by removing the fact that Bob is deathly allergic to latex. She cannot modify the Bob's encrypted and authenticated record, but she can easily update Bob's record with the encrypted record of a different patient Charlie. Likewise, if she want's Bob to pay Charlie's bill, she can replace Charlie's encrypted credit card number with Bob's, and once Bob is dead, he won't dispute the charges.

An encrypted-and-authenticated column value should be connected with its row in some way that Alice cannot circumvent. In the patch as you have it written, the client application can include row information in the patient record (specifically, the patient's name, ssn, etc) and verify when any patient record is retrieved that this information matches. But that's hardly "transparent" to the client. It's something all clients will have to do, and easy to forget to do in some code path. Also, for encrypted fixed-width columns, it is not an option. So it seems the client needs to "salt" (maybe not the right term for what I have in mind) the encryption with some relevant other columns, and that's something the libpq client would need to understand, and something the patch's syntax needs to support. Something like:

CREATE TABLE patient_records (
-- Cryptographically connected to the encrypted record
patient_id BIGINT NOT NULL,
patient_ssn CHAR(11),

-- The encrypted record
patient_record TEXT ENCRYPTED WITH (column_encryption_key = cek1,
column_encryption_salt = (patient_id, patient_ssn)),

-- Extra stuff, not cryptographically connected to anything
next_of_kin TEXT,
phone_number BIGINT,
...
);

I have not selected any algorithms that include such "salt"ing (again, maybe the wrong word) because I'm just trying to discuss the general feature, not get into the weeds about which cryptographic algorithm to select.

Thoughts?


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#54Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Mark Dilger (#53)
Re: Transparent column encryption

On Jan 10, 2023, at 9:26 AM, Mark Dilger <mark.dilger@enterprisedb.com> wrote:

-- Cryptographically connected to the encrypted record
patient_id BIGINT NOT NULL,
patient_ssn CHAR(11),

-- The encrypted record
patient_record TEXT ENCRYPTED WITH (column_encryption_key = cek1,
column_encryption_salt = (patient_id, patient_ssn)),

As you mention upthread, tying columns together creates problems for statements that only operate on a subset of columns. Allowing schema designers a choice about tying the encrypted column to zero or more other columns allows them to choose which works best for their security needs.

The example above would make a statement like "UPDATE patient_record SET patient_record = $1 \bind '{some json whatever}'" raise an exception at the libpq client level, but maybe that's what schema designers wants it to do. If not, they should omit the column_encryption_salt option in the create table statement; but if so, they should expect to have to specify the other columns as part of the update statement, possibly as part of the where clause, like

UPDATE patient_record
SET patient_record = $1
WHERE patient_id = 12345
AND patient_ssn = '111-11-1111'
\bind '{some json record}'

and have the libpq get the salt column values from the where clause (which may be tricky to implement), or perhaps use some new bind syntax like

UPDATE patient_record
SET patient_record = ($1:$2,$3) -- new, wonky syntax
WHERE patient_id = $2
AND patient_ssn = $3
\bind '{some json record}' 12345 '111-11-1111'

which would be error prone, since the sql statement could specify the ($1:$2,$3) inconsistently with the where clause, or perhaps specify the "new" salt columns even when not changed, like

UPDATE patient_record
SET patient_record = $1, patient_id = 12345, patient_ssn = "111-11-1111"
WHERE patient_id = 12345
AND patient_ssn = "111-11-1111"
\bind '{some json record}'

which looks kind of nuts at first glance, but is grammatically consistent with cases where one or both of the patient_id or patient_ssn are also being changed, like

UPDATE patient_record
SET patient_record = $1, patient_id = 98765, patient_ssn = "999-99-9999"
WHERE patient_id = 12345
AND patient_ssn = "111-11-1111"
\bind '{some json record}'

Or, of course, you can ignore these suggestions or punt them to some future patch that extends the current work, rather than trying to get it all done in the first column encryption commit. But it seems useful to think about what future directions would be, to avoid coding ourselves into a corner, making such future work harder.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#55vignesh C
vignesh21@gmail.com
In reply to: Peter Eisentraut (#51)
Re: Transparent column encryption

On Sat, 31 Dec 2022 at 19:47, Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

On 21.12.22 06:46, Peter Eisentraut wrote:

And another update. The main changes are that I added an 'unspecified'
CMK algorithm, which indicates that the external KMS knows what it is
but the database system doesn't. This was discussed a while ago. I
also changed some details about how the "cmklookup" works in libpq. Also
added more code comments and documentation and rearranged some code.

According to my local todo list, this patch is now complete.

Another update, with some merge conflicts resolved. I also fixed up the
remaining TODO markers in the code, which had something to do with Perl
and Windows. I did some more work on schema handling, e.g., CREATE
TABLE / LIKE, views, partitioning etc. on top of encrypted columns,
mostly tedious and repetitive, nothing interesting. I also rewrote the
code that extracts the underlying tables and columns corresponding to
query parameters. It's now much simpler and better encapsulated.

The patch does not apply on top of HEAD as in [1]http://cfbot.cputube.org/patch_41_3718.log, please post a rebased patch:
=== Applying patches on top of PostgreSQL commit ID
5f6401f81cb24bd3930e0dc589fc4aa8b5424cdc ===
=== applying patch ./v14-0001-Transparent-column-encryption.patch
....
Hunk #1 FAILED at 1109.
....
1 out of 5 hunks FAILED -- saving rejects to file doc/src/sgml/protocol.sgml.rej
....
patching file doc/src/sgml/ref/create_table.sgml
Hunk #3 FAILED at 351.
Hunk #4 FAILED at 704.
2 out of 4 hunks FAILED -- saving rejects to file
doc/src/sgml/ref/create_table.sgml.rej
....
Hunk #1 FAILED at 1420.
Hunk #2 FAILED at 4022.
2 out of 2 hunks FAILED -- saving rejects to file
doc/src/sgml/ref/psql-ref.sgml.rej

[1]: http://cfbot.cputube.org/patch_41_3718.log

Regards,
Vignesh

#56Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Mark Dilger (#53)
Re: Transparent column encryption

On 10.01.23 18:26, Mark Dilger wrote:

I wonder if logical replication could be made to work more easily with this feature. Specifically, subscribers of encrypted columns will need the encrypted column encryption key (CEK) and the name of the column master key (CMD) as exists on the publisher, but getting access to that is not automated as far as I can see. It doesn't come through automatically as part of a subscription, and publisher's can't publish the pg_catalog tables where the keys are kept (because publishing system tables is not supported.) Is it reasonable to make available the CEK and CMK to subscribers in an automated fashion, to facilitate setting up logical replication with less manual distribution of key information? Is this already done, and I'm just not recognizing that you've done it?

This would be done as part of DDL replication.

Can we do anything about the attack vector wherein a malicious DBA simply copies the encrypted datum from one row to another?

We discussed this earlier [0]/messages/by-id/4fbcf5540633699fc3d81ffb59cb0ac884673a7c.camel@vmware.com. This patch is not that feature. We
could get there eventually, but it would appear to be an immense amount
of additional work. We have to start somewhere.

[0]: /messages/by-id/4fbcf5540633699fc3d81ffb59cb0ac884673a7c.camel@vmware.com
/messages/by-id/4fbcf5540633699fc3d81ffb59cb0ac884673a7c.camel@vmware.com

#57Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#51)
1 attachment(s)
Re: Transparent column encryption

On 12/31/22 06:17, Peter Eisentraut wrote:

On 21.12.22 06:46, Peter Eisentraut wrote:

And another update.  The main changes are that I added an 'unspecified'
CMK algorithm, which indicates that the external KMS knows what it is
but the database system doesn't.  This was discussed a while ago.  I
also changed some details about how the "cmklookup" works in libpq. Also
added more code comments and documentation and rearranged some code.

Trying to delay a review until I had "completed it" has only led to me
not reviewing, so here's a partial one. Let me know what pieces of the
implementation and/or architecture you're hoping to get more feedback on.

I like the existing "caveats" documentation, and I've attached a sample
patch with some more caveats documented, based on some of the upthread
conversation:

- text format makes fixed-length columns leak length information too
- you only get partial protection against the Evil DBA
- RSA-OAEP public key safety

(Feel free to use/remix/discard as desired.)

When writing the paragraph on RSA-OAEP I was reminded that we didn't
really dig into the asymmetric/symmetric discussion. Assuming that most
first-time users will pick the builtin CMK encryption method, do we
still want to have an asymmetric scheme implemented first instead of a
symmetric keywrap? I'm still concerned about that public key, since it
can't really be made public. (And now that "unspecified" is available, I
think an asymmetric CMK could be easily created by users that have a
niche use case, and then we wouldn't have to commit to supporting it
forever.)

For the padding caveat:

+      There is no concern if all values are of the same length (e.g., credit
+      card numbers).

I nodded along to this statement last year, and then this year I learned
that CCNs aren't fixed-length. So with a 16-byte block, you're probably
going to be able to figure out who has an American Express card.

The column encryption algorithm is set per-column -- but isn't it
tightly coupled to the CEK, since the key length has to match? From a
layperson perspective, using the same key to encrypt the same plaintext
under two different algorithms (if they happen to have the same key
length) seems like it might be cryptographically risky. Is there a
reason I should be encouraged to do that?

With the loss of \gencr it looks like we also lost a potential way to
force encryption from within psql. Any plans to add that for v1?

While testing, I forgot how the new option worked and connected with
`column_encryption=on` -- and then I accidentally sent unencrypted data
to the server, since `on` means "not enabled". :( The server errors out
after the damage is done, of course, but would it be okay to strictly
validate that option's values?

Are there plans to document client-side implementation requirements, to
ensure cross-client compatibility? Things like the "PG\x00\x01"
associated data are buried at the moment (or else I've missed them in
the docs). If you're holding off until the feature is more finalized,
that's fine too.

Speaking of cross-client compatibility, I'm still disconcerted by the
ability to write the value "hello world" into an encrypted integer
column. Should clients be required to validate the text format, using
the attrealtypid?

It occurred to me when looking at the "unspecified" CMK scheme that the
CEK doesn't really have to be an encryption key at all. In that case it
can function more like a (possibly signed?) cookie for lookup, or even
be ignored altogether if you don't want to use a wrapping scheme
(similar to JWE's "direct" mode, maybe?). So now you have three ways to
look up or determine a column encryption key (CMK realm, CMK name, CEK
cookie)... is that a concept worth exploring in v1 and/or the documentation?

Thanks,
--Jacob

Attachments:

caveats.diff.txttext/plain; charset=UTF-8; name=caveats.diff.txtDownload
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 55f33a2f5f..06e1c077d5 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1588,7 +1588,33 @@ export PGCMKLOOKUP
     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.
+    How to do that securely is beyond the scope of this manual.  Note that
+    column encryption is applied to the text representation of the stored value,
+    so length differences can be leaked even for fixed-length column types (e.g.
+    <literal>bigint</literal>, whose largest decimal representation is longer
+    than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to a
+    stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access may still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as long
+    as they were encrypted with the same key.  Attackers may also remove values
+    by replacing them with nulls, and users with ownership over the table schema
+    may replace encryption keys or strip encryption from the columns entirely.
+    All of this is to say: proper access control is still of vital importance
+    when using this feature.
+   </para>
+
+   <para>
+    When using the RSA-OAEP CEK encryption methods, the "public" half of the CMK
+    may be used to replace existing column encryption keys with keys of an
+    attacker's choosing, compromising confidentiality and authenticity for
+    values encrypted under that CMK.  For this reason, it's important to keep
+    both the private <emphasis>and</emphasis> public halves of the CMK keypair
+    confidential.
    </para>
 
    <note>
#58Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: vignesh C (#55)
1 attachment(s)
Re: Transparent column encryption

On 11.01.23 17:46, vignesh C wrote:

On Sat, 31 Dec 2022 at 19:47, Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

On 21.12.22 06:46, Peter Eisentraut wrote:

And another update. The main changes are that I added an 'unspecified'
CMK algorithm, which indicates that the external KMS knows what it is
but the database system doesn't. This was discussed a while ago. I
also changed some details about how the "cmklookup" works in libpq. Also
added more code comments and documentation and rearranged some code.

According to my local todo list, this patch is now complete.

Another update, with some merge conflicts resolved. I also fixed up the
remaining TODO markers in the code, which had something to do with Perl
and Windows. I did some more work on schema handling, e.g., CREATE
TABLE / LIKE, views, partitioning etc. on top of encrypted columns,
mostly tedious and repetitive, nothing interesting. I also rewrote the
code that extracts the underlying tables and columns corresponding to
query parameters. It's now much simpler and better encapsulated.

The patch does not apply on top of HEAD as in [1], please post a rebased patch:

Here is a new patch. Changes since v14:

- Fixed some typos (review by Justin Pryzby)
- Fixed backward compat. psql and pg_dump (review by Justin Pryzby)
- Doc additions (review by Jacob Champion)
- Validate column_encryption option in libpq (review by Jacob Champion)
- Handle column encryption in inheritance
- Change CEKs and CMKs to live inside schemas

Attachments:

v15-0001-Transparent-column-encryption.patchtext/plain; charset=UTF-8; name=v15-0001-Transparent-column-encryption.patchDownload
From 0a40fa00a2426d74519415e9089ce7af7df4e012 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 25 Jan 2023 19:40:05 +0100
Subject: [PATCH v15] 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 asymmetric 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
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.

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).  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.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this
transparently if the \bind command is used.

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.

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                    | 295 ++++++
 doc/src/sgml/charset.sgml                     |   9 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 416 +++++++++
 doc/src/sgml/func.sgml                        |  26 +
 doc/src/sgml/glossary.sgml                    |  23 +
 doc/src/sgml/libpq.sgml                       | 315 +++++++
 doc/src/sgml/protocol.sgml                    | 442 +++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |   9 +
 .../ref/create_column_encryption_key.sgml     | 169 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  54 +-
 .../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                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   7 +
 src/backend/access/common/printtup.c          | 221 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  12 +
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 288 ++++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  17 +
 src/backend/commands/colenccmds.c             | 427 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 190 +++-
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 169 +++-
 src/backend/parser/parse_param.c              | 144 +++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  53 ++
 src/backend/utils/adt/varlena.c               | 106 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  42 +
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 343 ++++++-
 src/bin/pg_dump/pg_dump.h                     |  33 +
 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              |  52 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 179 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  55 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 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_colenckey.h            |  41 +
 src/include/catalog/pg_colenckeydata.h        |  46 +
 src/include/catalog/pg_colmasterkey.h         |  46 +
 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               |  41 +
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  40 +-
 src/include/parser/kwlist.h                   |   2 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   6 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 842 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 665 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 139 ++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  24 +
 .../t/001_column_encryption.pl                | 255 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 322 +++++++
 src/test/regress/expected/object_address.out  |  37 +-
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 232 +++++
 src/test/regress/sql/object_address.sql       |  13 +-
 src/test/regress/sql/type_sanity.sql          |   2 +
 138 files changed, 9319 insertions(+), 81 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/common/colenc.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/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 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 2df6559acc..bd1a0185ed 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 c1e4048054..1b8e1326c7 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>int4</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>
@@ -2467,6 +2516,252 @@ <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>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </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>int4</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>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </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/charset.sgml b/doc/src/sgml/charset.sgml
index 3032392b80..e3d21e66f8 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,15 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When transparent column encryption is used, then no encoding conversion
+     is possible.  (The encoding conversion happens on the server, and the
+     server cannot look inside any encrypted column values.)  If column
+     encryption is enabled for a session, then the server enforces that the
+     client encoding matches the server encoding, and any attempts to change
+     the client encoding will be rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..7494ec85d5 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <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
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support transparent column encryption or have disabled
+    it will see the encrypted values in this format.  Clients that support
+    transparent data encryption will not see these types in result sets, as
+    the protocol layer will translate them back to the 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 8dc8d7a0ce..fb3c19b01d 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,422 @@ <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>
+   Transparent 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 asymmetric 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 from making
+   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>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</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>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</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"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</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 transport 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>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation).
+   </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, 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.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on 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/func.sgml b/doc/src/sgml/func.sgml
index e09e289a43..85b5cb1450 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -23335,6 +23335,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 7c01a541fe..a9eb49b15f 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,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 0e7ae70c70..a34d694c2a 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,141 @@ <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 <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, 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>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </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>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </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 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 --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2999,25 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this byte to one to force encryption.
+            For example, use the C code literal <literal>0x10</literal> to
+            specify text format with forced encryption.  If the array pointer
+            is null then encryption is not forced for any parameter.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter 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 compromised 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.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3030,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3189,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</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 *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> 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>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4077,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>
 
@@ -4059,6 +4280,31 @@ <title>Retrieving Query Result Information</title>
      </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-PQexecPreparedDescribed"/> 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>
 
@@ -4584,6 +4830,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4838,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4895,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4956,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</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 PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> 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-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5045,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8079,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 93fc7167d4..c888811ad0 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,75 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-transparent-column-encryption">
+   <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, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column encryption protocol option is enabled.  That way, a client library
+    only needs to support decrypting data sent in binary and does not have to
+    support decoding the text format of the encryption-related types (see
+    <xref linkend="datatype-encrypted"/>).
+   </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 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -4061,6 +4130,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-flow-transparent-column-encryption"/>.)
+     </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>Int32</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-flow-transparent-column-encryption"/>.)
+     </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>
@@ -5157,6 +5360,46 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref
+      linkend="protocol-flow-transparent-column-encryption"/>), 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>Int32</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>
 
@@ -5545,6 +5788,35 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref
+      linkend="protocol-flow-transparent-column-encryption"/>), 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>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7370,6 +7642,176 @@ <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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</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>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</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>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</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>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</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>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</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 version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </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 IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </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 e90a0e1f83..331b1f010b 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 0000000000..655e1e00d8
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+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 }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</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
+   schema.  (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 (optionally schema-qualified) 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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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 0000000000..7f0e656ef0
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+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> ( REALM = <replaceable>realm</replaceable> )
+
+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 }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </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 schema.
+   (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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 7499da1d62..995486baf2 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..8c461b8f15 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,15 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support transparent column encryption or
+    decryption; its input or output data will always be the ciphertext.  This
+    is usually suitable for backups (see also <xref linkend="app-pgdump"/>).
+    If transparent encryption or decryption is wanted,
+    <command>INSERT</command> and <command>SELECT</command> need to be used
+    instead to write and read the data.
+   </para>
+
  </refsect1>
 
  <refsect1>
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 0000000000..e10b61267e
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,169 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></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.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </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 0000000000..6aaa1088d1
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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 a03dee4afe..6a9e28c2d7 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 | DEFAULT } ] [ 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 | DEFAULT } ] [ 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> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,46 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <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 id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -704,6 +744,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</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 0000000000..f2ac1beb08
--- /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 (optionally schema-qualified) 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 0000000000..fae95e09d1
--- /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 (optionally schema-qualified) 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 2c938cd7e1..5b3cbf919c 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -715,6 +715,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 ... \bind ... \g 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 e62d05e5ab..4bf60c729f 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 dc6528dc11..e68b8440be 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4026,6 +4054,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <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 id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index a3b743e8c1..e1425c222f 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 ef818228ac..a00545080d 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_sendint32(&buf, 0);	/* CEK alg */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 72faeb5dfa..eb93c4bb3f 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,156 @@ 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;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	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_sendint32(&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;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	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 +332,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 +349,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(int32));	/* attencalg */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +369,8 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +402,28 @@ 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);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +431,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_writeint32(buf, attencalg);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +469,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 72a2c3d3db..b869739b43 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 24bab58499..cc48069932 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 a60107bf94..7b9575635b 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 c4232344aa..d835e84134 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.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"
@@ -2798,6 +2800,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:
@@ -2828,6 +2833,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;
@@ -2938,6 +2949,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:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 7acf654bf8..b963e660e9 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 */
@@ -1493,6 +1499,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 4f006820b8..63b1e180c0 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,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_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,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] = Int32GetDatum(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)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attrealtypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attrealtypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 14e57adee2..00f914bc5f 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -26,6 +26,8 @@
 #include "catalog/dependency.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1997,6 +1999,254 @@ OpfamilyIsVisible(Oid opfid)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4567,6 +4817,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(OpfamilyIsVisible(oid));
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 25c50d66fd..35dfc6fb3c 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,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		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,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		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
@@ -1029,6 +1086,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1108,6 +1175,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, 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));
@@ -2311,6 +2393,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2367,6 +2451,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2489,6 +2574,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2575,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3040,6 +3128,92 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				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);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				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 data of %s for %s"),
+								 getObjectDescription(&(ObjectAddress){ColumnEncKeyRelationId, cekdata->ckdcekid}, false),
+								 getObjectDescription(&(ObjectAddress){ColumnMasterKeyRelationId, cekdata->ckdcmkid}, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  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_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4440,6 +4614,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);
@@ -4905,6 +5091,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, 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",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, 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 48f7348f91..69f6175c60 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 bea51b3af1..59c23e9ef8 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"
@@ -118,6 +120,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -379,6 +387,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:
@@ -527,6 +537,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -643,6 +655,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:
@@ -876,6 +891,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 0000000000..0bb4948672
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,427 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, 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/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.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, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	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)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	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")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	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, int 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] = Int32GetDatum(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)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	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 (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(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);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			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\"",
+						   NameListToString(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)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	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);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	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;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	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)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d6c6d514f3..d53c283ad8 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attrealtypid,
+													orig_att->atttypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 82bda15889..b4c681005a 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +517,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..4c1628cf7b 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 42cced9ebe..4b5ac30441 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..93d61c96ad 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 1293545947..62454bc265 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"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,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);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,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);
 	}
@@ -2562,13 +2568,43 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				Oid			defCollId;
 
 				/*
-				 * Yes, try to merge the two column definitions. They must
-				 * have the same type, typmod, and collation.
+				 * Yes, try to merge the two column definitions.
 				 */
 				ereport(NOTICE,
 						(errmsg("merging multiple inherited definitions of column \"%s\"",
 								attributeName)));
 				def = (ColumnDef *) list_nth(inhSchema, exist_attno - 1);
+
+				/*
+				 * Check encryption parameter.  All parents must have the same
+				 * encryption settings for a column.
+				 */
+				if ((def->encryption && !attribute->attcek) ||
+					(!def->encryption && attribute->attcek))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && attribute->attcek)
+				{
+					/*
+					 * Merging the encryption properties of two encrypted
+					 * parent columns is not yet implemented.  Right now, this
+					 * would confuse the checks of the type etc. below (we
+					 * must check the physical and the real types against each
+					 * other, respectively), which might require a larger
+					 * restructuring.  For now, just give up here.
+					 */
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("multiple inheritance of encrypted columns is not implemented")));
+				}
+
+				/*
+				 * Must have the same type, typmod, and collation.
+				 */
 				typenameTypeIdAndMod(NULL, def->typeName, &defTypeId, &deftypmod);
 				if (defTypeId != attribute->atttypid ||
 					deftypmod != attribute->atttypmod)
@@ -2641,6 +2677,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attrealtypid,
+														attribute->atttypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -2919,6 +2961,34 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								 errdetail("%s versus %s", def->compression, newdef->compression)));
 				}
 
+				/*
+				 * Check encryption parameter.  All parents and children must
+				 * have the same encryption settings for a column.
+				 */
+				if ((def->encryption && !newdef->encryption) ||
+					(!def->encryption && newdef->encryption))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && newdef->encryption)
+				{
+					FormData_pg_attribute a, newa;
+
+					GetColumnEncryption(def->encryption, &a);
+					GetColumnEncryption(newdef->encryption, &newa);
+
+					if (a.atttypid != newa.atttypid ||
+						a.attcek != newa.attcek ||
+						a.attencalg != newa.attencalg)
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" has an encryption specification conflict",
+										attributeName)));
+				}
+
 				/* Mark the column as locally defined */
 				def->is_local = true;
 				/* Merge of NOT NULL constraints = OR 'em together */
@@ -6844,6 +6914,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);
 
@@ -12675,6 +12753,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:
@@ -19274,3 +19355,108 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	List	   *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 = defGetQualifiedName(el);
+		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);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				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 = get_cek_oid(cek, false);
+
+	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;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	List	   *result;
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	char	   *nspname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attr->attcek));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attr->attcek);
+
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+	nspname = get_namespace_name(form->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(form->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->attencalg))),
+									-1));
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index bb0f5de4c2..cec2daa9c3 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/commands/view.c b/src/backend/commands/view.c
index ff98c773f5..c93e2faa94 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attrealtypid,
+													orig_att->atttypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 8fc24c882b..2cd32b3637 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3982,6 +3982,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/gram.y b/src/backend/parser/gram.y
index a0138382a1..8d0727cac6 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,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
@@ -592,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
@@ -690,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
@@ -714,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
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -3700,14 +3705,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;
@@ -3716,8 +3722,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;
@@ -3774,6 +3780,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4045,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6282,33 @@ 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
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (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; }
@@ -6289,6 +6328,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)); }
@@ -6800,6 +6843,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -9140,6 +9185,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9817,6 +9882,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10148,6 +10233,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11304,6 +11407,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_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 any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16940,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16854,6 +17004,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17488,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17425,6 +17577,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 2240284f21..bcfb2d087f 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,146 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(te->expr, Param))
+			{
+				Param	   *p = (Param *) te->expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(te->expr, Var))
+			{
+				Var	   *v = (Var *) te->expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell *lc;
+
+					foreach (lc, srcrte->values_lists)
+					{
+						List *values_list = lfirst_node(List, lc);
+						Node *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach (lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f9218f48aa..00492e1eaf 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1035,8 +1035,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attrealtypid,
+												attribute->atttypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 5b775cf7d0..958fd65ac4 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2209,12 +2209,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 470b734e9e..e20ab8966c 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -71,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"
 
@@ -1812,6 +1814,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 %d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2553,6 +2565,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2611,11 +2625,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	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 = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				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 + 1)));
+
+			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_sendint32(&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 c7d9d96b45..0a17eb4486 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,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,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 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,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 +2665,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 +2791,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 +3100,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3733,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index fd81c47474..af0bca7d3e 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -686,6 +686,112 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c07382051d..7ad159110f 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
  *
@@ -3683,3 +3705,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+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;
+}
+
+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 77c2ba3f8f..d40af13efe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 94abede512..eb432260da 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"
@@ -219,6 +222,32 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAMENSP] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameNspIndexId,
+		KEY(Anum_pg_colenckey_cekname,
+			Anum_pg_colenckey_ceknamespace),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +262,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAMENSP] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameNspIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname,
+			Anum_pg_colmasterkey_cmknamespace),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 033647011b..4b6be68f27 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"
@@ -130,6 +132,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())
 	{
 		/*
@@ -237,6 +245,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,
@@ -297,7 +311,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 a43f2e5553..e67462c81a 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -201,6 +203,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);
 
@@ -859,6 +867,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,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 ba5e6acbbb..7f55eb2716 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3430,6 +3430,8 @@ _getObjectDescription(PQExpBuffer buf, const 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, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 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 527c7651ab..e09eef540d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,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);
@@ -393,6 +396,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},
@@ -685,6 +689,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");
 
@@ -1057,6 +1064,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"
@@ -5572,6 +5580,144 @@ 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_ceknamespace;
+	int			i_cekowner;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, 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_ceknamespace = PQfnumber(res, "ceknamespace");
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, 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].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			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_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, 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_cmknamespace = PQfnumber(res, "cmknamespace");
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		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
@@ -8188,6 +8334,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8297,20 +8446,35 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	if (fout->remoteVersion >= 120000)
 		appendPQExpBufferStr(q,
-							 "a.attgenerated\n");
+							 "a.attgenerated,\n");
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "'' AS attgenerated,\n");
+
+	if (fout->remoteVersion >= 160000)
+		appendPQExpBuffer(q,
+						  "a.attcek,\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,
+							 "NULL AS attcek,\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 (%s = t.oid)\n"
 					  "WHERE a.attnum > 0::pg_catalog.int2\n"
 					  "ORDER BY a.attrelid, a.attnum",
-					  tbloids->data);
+					  tbloids->data,
+					  fout->remoteVersion >= 160000 ?
+					  "CASE WHEN a.attrealtypid <> 0 THEN a.attrealtypid ELSE a.atttypid END" :
+					  "a.atttypid");
 
 	res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
 
@@ -8326,6 +8490,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	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");
@@ -8387,6 +8554,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->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		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));
@@ -8415,6 +8585,22 @@ 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_attcek))
+			{
+				Oid		attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[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));
@@ -9933,6 +10119,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;
@@ -13326,6 +13518,131 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * 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",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_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,
+								  .namespace = cekinfo->dobj.namespace->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,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, 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",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	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,
+								  .namespace = cmkinfo->dobj.namespace->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,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15409,6 +15726,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[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_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -17972,6 +18305,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 e7cbd8d7ed..99b6b20ced 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;
+	struct _CekInfo **attcek;
+	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: */
+	struct _CmkInfo **cekcmks;
+	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
  */
@@ -684,6 +713,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -711,6 +742,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 f963b9a449..d56372cd1c 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 fbd1c6fc85..f35f3b26ae 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 d92247c915..dec2a0d305 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -645,6 +645,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, 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, },
@@ -1245,6 +1257,26 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1663,6 +1695,26 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index b5201edf55..9e940dca6e 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c8a0bb7b3a..e004e30d86 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1530,7 +1530,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;
@@ -1546,6 +1546,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;
@@ -1568,6 +1569,7 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attrealtypid;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1844,7 +1846,11 @@ 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)");
+	if (pset.sversion >= 160000)
+		attrealtypid = "CASE WHEN a.attrealtypid <> 0 THEN a.attrealtypid ELSE a.atttypid END";
+	else
+		attrealtypid = "a.atttypid";
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, a.atttypmod)", attrealtypid);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1857,8 +1863,10 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		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");
+		appendPQExpBuffer(&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 = %s AND a.attcollation <> t.typcollation) AS attcollation",
+						  attrealtypid);
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
 			appendPQExpBufferStr(&buf, ",\n  a.attidentity");
@@ -1909,6 +1917,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			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 ||
@@ -2032,6 +2052,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)
@@ -2124,6 +2146,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),
@@ -4477,6 +4510,144 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 554fe86725..1cf8f72176 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1729966959 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,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 73d4b393bc..010bc5a6d5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,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 5a28b6f713..6736505c3a 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 5e1882eaea..fc8ed212b6 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -933,6 +933,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1223,6 +1237,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
@@ -1705,7 +1721,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1952,6 +1968,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_SCHEMA_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", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2894,6 +2926,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",
@@ -3619,6 +3671,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/common/Makefile b/src/common/Makefile
index 2f424a5735..c3ac6cbc35 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -48,6 +48,7 @@ LIBS += $(PTHREAD_LIBS)
 OBJS_COMMON = \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		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";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index 1caa1fed04..4ef62bddf2 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -3,6 +3,7 @@
 common_sources = files(
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 747ecb800d..d199033651 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 ffd5e9dc82..eb59e73c0a 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/heap.h b/src/include/catalog/heap.h
index d01ab504b6..758696b539 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 3179be09d3..9e2c5256f7 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,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/namespace.h b/src/include/catalog/namespace.h
index f64a0ec26b..d0b9e8458d 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -115,6 +115,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c4d6adcd3e..c58b79e3a7 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 5b950129de..0e9e85ebf3 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 b561e17781..4b5b5ad32f 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) */
+	int32		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_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..be44c0d70d
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,41 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	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_nsp_index, 8242, ColumnEncKeyNameNspIndexId, on pg_colenckey using btree(cekname name_ops, ceknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /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-2023, 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);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..a4a103bbed
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	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_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, on pg_colmasterkey using btree(cmkname name_ops, cmknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c867d99563..ff06d52fd0 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 b2cdea66c4..114279fa64 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 91587b99d0..c21052a3f7 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 c0f2a8a77c..11bf7e05f1 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6346,6 +6346,14 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -11918,4 +11926,37 @@
   prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary',
   prosrc => 'brin_minmax_multi_summary_send' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_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 92bcaf2c73..85e4b5554e 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 => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 519e570c8c..3c7ab2a8fe 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 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, 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);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e7c2b91a58..11f88c59ce 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,4 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml>.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 8c70b2fd5b..6cf4b65500 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 89335d95e7..abf59d0187 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -715,6 +715,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? */
@@ -751,11 +752,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1949,6 +1951,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2136,6 +2141,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..c8cf9c7776 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_param.h b/src/include/parser/parse_param.h
index d4865e50f6..26e1cd617a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..c1f6fe1410 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/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..1dac24e4b4 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,9 @@ 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 char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, 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 a443181d41..8ecacb29df 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d5d50ceab4..bf1d527c1a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAMENSP,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAMENSP,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c18e914228..10dfda8016 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 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
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 50b5df3490..bf6149e859 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}
@@ -1412,6 +1420,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4029,6 +4059,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 0000000000..ab6c70967f
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,842 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, 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 "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = 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;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								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);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05>
+ * for encrypting and decrypting column values.
+ */
+
+#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,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+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;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+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;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+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,
+					 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;
+	int64		al;
+	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;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	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;
+}
+
+/*
+ * Decrypt a column value
+ */
+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			iv_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 = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_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;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_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;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#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)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "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 */
+
+/*
+ * Encrypt a column value
+ */
+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			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_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)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	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, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								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,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, 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, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, 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, const char *cmkfilename, 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 ec62550e38..7c7e2bac12 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,414 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+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 *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					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, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', 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)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, "r");
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+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
@@ -1251,13 +1662,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				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;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				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;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* 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;
@@ -1500,6 +1949,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1967,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2006,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2122,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2165,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2264,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2313,47 @@ PQsendQueryGuts(PGconn *conn,
 		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;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2372,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2390,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#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)
+				{
+					libpq_append_conn_error(conn, "column encryption key not found");
+					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
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2872,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4140,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4188,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)
 {
@@ -3762,6 +4384,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 8ab6a88416..3e3d8674be 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);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -547,6 +555,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) ||
@@ -561,6 +571,21 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, 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.
@@ -582,8 +607,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;
 	}
 
@@ -685,10 +712,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, 4, 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! */
@@ -1468,6 +1516,92 @@ 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);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	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;
+	int			ret;
+
+	/* 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, 4, 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 */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2420,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 abaab6a073..77791e8349 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);
+			pqTraceOutputInt32(f, message, cursor, false);
+			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);
+			pqTraceOutputInt32(f, message, cursor, false);
+		}
 	}
 }
 
@@ -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);
+	pqTraceOutputInt32(f, message, cursor, false);
+	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 f3d9220496..cf339a3e53 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,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,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 +559,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 d94b648ea5..3dfedc0f9b 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);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 573fd9b6ea..a6e6b2ada9 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,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
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': get_option('ssl')},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 4df544ecef..0c36aa5f32 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-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_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext pqInternalNotice:2
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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 b2a4b06fd2..87d2808b52 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c8ba170503 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 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..d5ead874e5
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, 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
+
+export OPENSSL PERL
+
+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 0000000000..47f88c41ce
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,24 @@
+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',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+      'PERL': perl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..c56af737ac
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,255 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+my $perl = $ENV{PERL};
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$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"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	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}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# 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\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[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 %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = "*=run:$perl ./test_run_decrypt.pl %k %a %p";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$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\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$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 update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$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', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+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');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$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 tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$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 second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..14eafb8ec9
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, 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 $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# 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, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+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, algorithm = '$cmkalg', 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 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	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;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	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 = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	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
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/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-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $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}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..d55ddb1ab7 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,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 0000000000..354f0bb1e3
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,322 @@
+\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;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+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       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column 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 in schema "public"
+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 in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION '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 SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index 25c174f275..08b76b6cc8 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -107,7 +109,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
@@ -327,6 +330,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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+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},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 -- these object types cannot be qualified names
 SELECT pg_get_object_address('language', '{one}', '{}');
 ERROR:  language "one" does not exist
@@ -409,6 +430,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}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,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
@@ -547,6 +576,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
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|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 215eb899be..c34fdb1b6c 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,9 @@ 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 {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {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 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ 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)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,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
@@ -872,6 +876,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
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 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,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index a930dfe48c..8154dc49ca 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -103,7 +103,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 427429975e..57c7d2bec3 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..3f5fcaba92
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,232 @@
+\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;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+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
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+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 SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..35af43032f 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -99,7 +101,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
@@ -174,6 +177,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}', '{}'),
@@ -228,6 +234,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;
 
@@ -247,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
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 642e8821d713d75f142ef4eda14e490ba0fb810b
-- 
2.39.1

#59Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Justin Pryzby (#52)
Re: Transparent column encryption

On 07.01.23 01:34, Justin Pryzby wrote:

"ON (CASE WHEN a.attrealtypid <> 0 THEN a.attrealtypid ELSE a.atttypid END = t.oid)\n"

This breaks interoperability with older servers:
ERROR: column a.attrealtypid does not exist

Same in describe.c

Find attached some typos and bad indentation. I'm sending this off now
as I've already sat on it for 2 weeks since starting to look at the
patch.

Thanks, I have integrated all that into the v15 patch I just posted.

#60Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#56)
Re: Transparent column encryption

On 12.01.23 17:32, Peter Eisentraut wrote:

Can we do anything about the attack vector wherein a malicious DBA
simply copies the encrypted datum from one row to another?

We discussed this earlier [0].  This patch is not that feature.  We
could get there eventually, but it would appear to be an immense amount
of additional work.  We have to start somewhere.

I've been thinking, this could be done as a "version 2" of the currently
proposed feature, within the same framework. We'd extend the
RowDescription and ParameterDescription messages to provide primary key
information, some flags, then the client would have enough to know what
to do. As you wrote in your follow-up message, a challenge would be to
handle statements that do not touch all the columns. We'd need to work
through this and consider all the details.

#61Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#57)
Re: Transparent column encryption

On 19.01.23 21:48, Jacob Champion wrote:

I like the existing "caveats" documentation, and I've attached a sample
patch with some more caveats documented, based on some of the upthread
conversation:

- text format makes fixed-length columns leak length information too
- you only get partial protection against the Evil DBA
- RSA-OAEP public key safety

(Feel free to use/remix/discard as desired.)

I have added those in the v15 patch I just posted.

When writing the paragraph on RSA-OAEP I was reminded that we didn't
really dig into the asymmetric/symmetric discussion. Assuming that most
first-time users will pick the builtin CMK encryption method, do we
still want to have an asymmetric scheme implemented first instead of a
symmetric keywrap? I'm still concerned about that public key, since it
can't really be made public.

I had started coding that, but one problem was that the openssl CLI
doesn't really provide any means to work with those kinds of keys. The
"openssl enc" command always wants to mix in a password. Without that,
there is no way to write a test case, and more crucially no way for
users to set up these kinds of keys. Unless we write our own tooling
for this, which, you know, the patch just passed 400k in size.

For the padding caveat:

+      There is no concern if all values are of the same length (e.g., credit
+      card numbers).

I nodded along to this statement last year, and then this year I learned
that CCNs aren't fixed-length. So with a 16-byte block, you're probably
going to be able to figure out who has an American Express card.

Heh. I have removed that parenthetical remark.

The column encryption algorithm is set per-column -- but isn't it
tightly coupled to the CEK, since the key length has to match? From a
layperson perspective, using the same key to encrypt the same plaintext
under two different algorithms (if they happen to have the same key
length) seems like it might be cryptographically risky. Is there a
reason I should be encouraged to do that?

Not really. I was also initially confused by this setup, but that's how
other similar systems are set up, so I thought it would be confusing to
do it differently.

With the loss of \gencr it looks like we also lost a potential way to
force encryption from within psql. Any plans to add that for v1?

\gencr didn't do that either. We could do it. The libpq API supports
it. We just need to come up with some syntax for psql.

While testing, I forgot how the new option worked and connected with
`column_encryption=on` -- and then I accidentally sent unencrypted data
to the server, since `on` means "not enabled". :( The server errors out
after the damage is done, of course, but would it be okay to strictly
validate that option's values?

fixed in v15

Are there plans to document client-side implementation requirements, to
ensure cross-client compatibility? Things like the "PG\x00\x01"
associated data are buried at the moment (or else I've missed them in
the docs). If you're holding off until the feature is more finalized,
that's fine too.

This is documented in the protocol chapter, which I thought was the
right place. Did you want more documentation, or in a different place?

Speaking of cross-client compatibility, I'm still disconcerted by the
ability to write the value "hello world" into an encrypted integer
column. Should clients be required to validate the text format, using
the attrealtypid?

Well, we can ask them to, but we can't really require them, in a
cryptographic sense. I'm not sure what more we can do.

It occurred to me when looking at the "unspecified" CMK scheme that the
CEK doesn't really have to be an encryption key at all. In that case it
can function more like a (possibly signed?) cookie for lookup, or even
be ignored altogether if you don't want to use a wrapping scheme
(similar to JWE's "direct" mode, maybe?). So now you have three ways to
look up or determine a column encryption key (CMK realm, CMK name, CEK
cookie)... is that a concept worth exploring in v1 and/or the documentation?

I don't completely follow this.

#62Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#61)
Re: Transparent column encryption

On Wed, Jan 25, 2023 at 11:00 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

When writing the paragraph on RSA-OAEP I was reminded that we didn't
really dig into the asymmetric/symmetric discussion. Assuming that most
first-time users will pick the builtin CMK encryption method, do we
still want to have an asymmetric scheme implemented first instead of a
symmetric keywrap? I'm still concerned about that public key, since it
can't really be made public.

I had started coding that, but one problem was that the openssl CLI
doesn't really provide any means to work with those kinds of keys. The
"openssl enc" command always wants to mix in a password. Without that,
there is no way to write a test case, and more crucially no way for
users to set up these kinds of keys. Unless we write our own tooling
for this, which, you know, the patch just passed 400k in size.

Arrgh: https://github.com/openssl/openssl/issues/10605

The column encryption algorithm is set per-column -- but isn't it
tightly coupled to the CEK, since the key length has to match? From a
layperson perspective, using the same key to encrypt the same plaintext
under two different algorithms (if they happen to have the same key
length) seems like it might be cryptographically risky. Is there a
reason I should be encouraged to do that?

Not really. I was also initially confused by this setup, but that's how
other similar systems are set up, so I thought it would be confusing to
do it differently.

Which systems let you mix and match keys and algorithms this way? I'd
like to take a look at them.

With the loss of \gencr it looks like we also lost a potential way to
force encryption from within psql. Any plans to add that for v1?

\gencr didn't do that either. We could do it. The libpq API supports
it. We just need to come up with some syntax for psql.

Do you think people would rather set encryption for all parameters at
once -- something like \encbind -- or have the ability to mix
encrypted and unencrypted parameters?

Are there plans to document client-side implementation requirements, to
ensure cross-client compatibility? Things like the "PG\x00\x01"
associated data are buried at the moment (or else I've missed them in
the docs). If you're holding off until the feature is more finalized,
that's fine too.

This is documented in the protocol chapter, which I thought was the
right place. Did you want more documentation, or in a different place?

I just missed it; sorry.

Speaking of cross-client compatibility, I'm still disconcerted by the
ability to write the value "hello world" into an encrypted integer
column. Should clients be required to validate the text format, using
the attrealtypid?

Well, we can ask them to, but we can't really require them, in a
cryptographic sense. I'm not sure what more we can do.

Right -- I just mean that clients need to pay more attention to it
now, whereas before they may have delegated correctness to the server.
The problem is documented in the context of deterministic encryption,
but I think it applies to randomized as well.

More concretely: should psql allow you to push arbitrary text into an
encrypted \bind parameter, like it does now?

It occurred to me when looking at the "unspecified" CMK scheme that the
CEK doesn't really have to be an encryption key at all. In that case it
can function more like a (possibly signed?) cookie for lookup, or even
be ignored altogether if you don't want to use a wrapping scheme
(similar to JWE's "direct" mode, maybe?). So now you have three ways to
look up or determine a column encryption key (CMK realm, CMK name, CEK
cookie)... is that a concept worth exploring in v1 and/or the documentation?

I don't completely follow this.

Yeah, I'm not expressing it very well. My feeling is that the
organization system here -- a realm "contains" multiple CMKs, a CMK
encrypts multiple CEKs -- is so general and flexible that it may need
some suggested guardrails for people to use it sanely. I just don't
know what those guardrails should be. I was motivated by the
realization that CEKs don't even need to be keys.

Thanks,
--Jacob

#63Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Jacob Champion (#62)
Re: Transparent column encryption

On 30.01.23 23:30, Jacob Champion wrote:

The column encryption algorithm is set per-column -- but isn't it
tightly coupled to the CEK, since the key length has to match? From a
layperson perspective, using the same key to encrypt the same plaintext
under two different algorithms (if they happen to have the same key
length) seems like it might be cryptographically risky. Is there a
reason I should be encouraged to do that?

Not really. I was also initially confused by this setup, but that's how
other similar systems are set up, so I thought it would be confusing to
do it differently.

Which systems let you mix and match keys and algorithms this way? I'd
like to take a look at them.

See here for example:
https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-database-engine?view=sql-server-ver15

With the loss of \gencr it looks like we also lost a potential way to
force encryption from within psql. Any plans to add that for v1?

\gencr didn't do that either. We could do it. The libpq API supports
it. We just need to come up with some syntax for psql.

Do you think people would rather set encryption for all parameters at
once -- something like \encbind -- or have the ability to mix
encrypted and unencrypted parameters?

For pg_dump, I'd like a mode that makes all values parameters of an
INSERT statement. But obviously not all of those will be encrypted. So
I think we'd want a per-parameter syntax.

More concretely: should psql allow you to push arbitrary text into an
encrypted \bind parameter, like it does now?

We don't have any data type awareness like that now in psql or libpq.
It would be quite a change to start now. How would that deal with data
type extensibility, is an obvious question to start with. Don't know.

#64Jacob Champion
jchampion@timescale.com
In reply to: Peter Eisentraut (#63)
Re: Transparent column encryption

On Tue, Jan 31, 2023 at 5:26 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

See here for example:
https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-database-engine?view=sql-server-ver15

Hm. I notice they haven't implemented more than one algorithm, so I
wonder if they're going to be happy with their decision to
mix-and-match when number two arrives.

For pg_dump, I'd like a mode that makes all values parameters of an
INSERT statement. But obviously not all of those will be encrypted. So
I think we'd want a per-parameter syntax.

Makes sense. Maybe something that defaults to encrypted with opt-out
per parameter?

UPDATE t SET name = $1 WHERE id = $2
\encbind "Jacob" plaintext(24)

More concretely: should psql allow you to push arbitrary text into an
encrypted \bind parameter, like it does now?

We don't have any data type awareness like that now in psql or libpq.
It would be quite a change to start now.

I agree. It just feels weird that a misbehaving client can "attack"
the other client implementations using it, and we don't make any
attempt to mitigate it.

--Jacob

#65Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Peter Eisentraut (#58)
Re: Transparent column encryption

On Jan 25, 2023, at 10:44 AM, Peter Eisentraut <peter.eisentraut@enterprisedb.com> wrote:

Here is a new patch. Changes since v14:

- Fixed some typos (review by Justin Pryzby)
- Fixed backward compat. psql and pg_dump (review by Justin Pryzby)
- Doc additions (review by Jacob Champion)
- Validate column_encryption option in libpq (review by Jacob Champion)
- Handle column encryption in inheritance
- Change CEKs and CMKs to live inside schemas<v15-0001-Transparent-column-encryption.patch>

Thanks Peter. Here are some observations about the documentation in patch version 15.

In acronyms.sgml, the CEK and CMK entries should link to documentation, perhaps linkend="glossary-column-encryption-key" and linkend="glossary-column-master-key". These glossary entries should in turn link to linkend="ddl-column-encryption".

In ddl.sgml, the sentence "forcing encryption of certain parameters in the client library (see its documentation)" should link to linkend="libpq-connect-column-encryption".

Did you intend the use of "transparent data encryption" (rather than "transparent column encryption") in datatype.sgml? If so, what's the difference?

Is this feature intended to be available from ecpg? If so, can we maybe include an example in 36.3.4. Prepared Statements showing how to pass the encrypted values securely. If not, can we include verbiage about that limitation, so folks don't waste time trying to figure out how to do it?

The documentation for pg_dump (and pg_dumpall) now includes a --decrypt-encrypted-columns option, which I suppose requires cmklookup to first be configured, and for PGCMKLOOKUP to be exported. There isn't anything in the pg_dump docs about this, though, so maybe a link to section 5.5.3 with a warning about not running pg_dump this way on the database server itself?

How does a psql user mark a parameter as having forced encryption? A libpq user can specify this in the paramFormats array, but I don't see any syntax for doing this from psql. None of the perl tap tests you've included appear to do this (except indirectly when calling test_client); grep'ing for the libpq error message "parameter with forced encryption is not to be encrypted" in the tests has no matches. Is it just not possible? I thought you'd mentioned some syntax for this when we spoke in person, but I don't see it now.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#66Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Mark Dilger (#65)
Re: Transparent column encryption

On Feb 11, 2023, at 1:54 PM, Mark Dilger <mark.dilger@enterprisedb.com> wrote:

Here are some observations

I should mention, src/sgml/html/libpq-exec.html needs clarification:

paramFormats[]Specifies whether parameters are text (put a zero in the array entry for the corresponding parameter) or binary (put a one in the array entry for the corresponding parameter). If the array pointer is null then all parameters are presumed to be text strings.

Perhaps you should edit this last sentence to say that all parameters are presumed to be test strings without forced encryption.

Values passed in binary format require knowledge of the internal representation expected by the backend. For example, integers must be passed in network byte order. Passing numeric values requires knowledge of the server storage format, as implemented in src/backend/utils/adt/numeric.c::numeric_send() and src/backend/utils/adt/numeric.c::numeric_recv().

When column encryption is enabled, the second-least-significant byte of this parameter specifies whether encryption should be forced for a parameter.

The value 0x10 has a one as its second-least-significant *nibble*, but that's a really strange way to describe the high-order nibble, and perhaps not what you mean. Could you clarify?

Set this byte to one to force encryption.

I think setting the byte to one (0x01) means "binary unencrypted", not "force encryption". Don't you mean to set this byte to 16?

For example, use the C code literal 0x10 to specify text format with forced encryption. If the array pointer is null then encryption is not forced for any parameter.
If encryption is forced for a parameter but the parameter 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 compromised 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.)

I think you should say something about how specifying 0x11 will behave -- in other words, asking for encrypted binary data. I believe that this is will draw a "format must be text for encrypted parameter" error, and that the docs should clearly say so.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#67Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#58)
1 attachment(s)
Re: Transparent column encryption

New patch.

Per some feedback, I have renamed this feature. People didn't like the
"transparent", for various reasons. The new name I came up with is
"automatic client-side column-level encryption". This also matches the
terminology used in other products better. (Maybe the acronym ACSCLE --
pronounced "a chuckle" -- will catch on.) I'm also using various
subsets of that name when the context is clear.

Other changes since v15:

- CEKs and CMKs now have USAGE privileges. (There are some TODO markers
where I got too bored with boilerplate. I will fill those in, but the
idea should be clear.)

- Renamed attrealtypid to attusertypid. (It wasn't really "real".)
- Added corresponding attusertypmod.
- Removed attencalg, it's now stored in atttypmod.
(The last three together make the whole attribute storage work more
sensibly and smoothly.)

- Various documentation changes (review by Mark Dilger)
- Added more explicit documentation that this feature is not to protect
against an "evil DBA".

Attachments:

v16-0001-Automatic-client-side-column-level-encryption.patchtext/plain; charset=UTF-8; name=v16-0001-Automatic-client-side-column-level-encryption.patchDownload
From db66586f147abebba9d22ea416ae79032ebad5bb Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 22 Feb 2023 11:11:15 +0100
Subject: [PATCH v16] Automatic client-side column-level encryption

This feature enables the automatic 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 asymmetric 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
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.

To get automatically encrypted data into the database (as opposed to
reading it out), it is required to use protocol-level prepared
statements (i.e., extended query).  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.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this if the
\bind command is used.

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.

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                    | 317 +++++++
 doc/src/sgml/charset.sgml                     |  10 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 444 +++++++++
 doc/src/sgml/func.sgml                        |  26 +
 doc/src/sgml/glossary.sgml                    |  26 +
 doc/src/sgml/libpq.sgml                       | 322 +++++++
 doc/src/sgml/protocol.sgml                    | 441 +++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |  10 +
 .../ref/create_column_encryption_key.sgml     | 170 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  55 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/grant.sgml                   |  12 +-
 doc/src/sgml/ref/pg_dump.sgml                 |  42 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   7 +
 src/backend/access/common/printtup.c          | 222 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  60 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 288 ++++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  17 +
 src/backend/commands/colenccmds.c             | 439 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 204 ++++-
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 187 +++-
 src/backend/parser/parse_param.c              | 157 ++++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  53 ++
 src/backend/utils/adt/acl.c                   |  18 +
 src/backend/utils/adt/varlena.c               | 106 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  42 +
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/dumputils.c                   |   4 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 377 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  35 +
 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              |  72 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 191 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  67 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   2 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |  11 +
 src/include/catalog/pg_colenckey.h            |  46 +
 src/include/catalog/pg_colenckeydata.h        |  46 +
 src/include/catalog/pg_colmasterkey.h         |  47 +
 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               |  41 +
 src/include/catalog/pg_type.dat               |  12 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  43 +-
 src/include/parser/kwlist.h                   |   2 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   6 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 842 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 665 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 139 ++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  24 +
 .../t/001_column_encryption.pl                | 255 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 353 ++++++++
 src/test/regress/expected/object_address.out  |  37 +-
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 265 ++++++
 src/test/regress/sql/object_address.sql       |  13 +-
 src/test/regress/sql/type_sanity.sql          |   2 +
 142 files changed, 9686 insertions(+), 85 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/common/colenc.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/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 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 2df6559acc..3a5f2c254c 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; see <xref linkend="ddl-column-encryption"/>
+     </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; see <xref linkend="ddl-column-encryption"/>
+     </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 c1e4048054..808f29669d 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,44 @@ <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>attusertypid</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.  If the column is not
+       encrypted, then 0.  For encrypted columns, the field
+       <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.
+       </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type
+       modifier (analogous to <structfield>atttypmod</structfield>) that is
+       reported to the client.  If the column is not encrypted, then -1.  For
+       encrypted columns, the field <structfield>atttypmod</structfield>)
+       contains 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>
@@ -2467,6 +2520,270 @@ <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>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </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>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </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>int4</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>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </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>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml
index 3032392b80..f3026fff83 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,16 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When automatic client-side column-level encryption is used, then no
+     encoding conversion is possible.  (The encoding conversion happens on the
+     server, and the server cannot look inside any encrypted column values.)
+     If automatic client-side column-level encryption is enabled for a
+     session, then the server enforces that the client encoding matches the
+     server encoding, and any attempts to change the client encoding will be
+     rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..67fce16872 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <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
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support automatic client-side column-level encryption
+    or have disabled it will see the encrypted values in this format.  Clients
+    that support automatic client-side column-level encryption will not see
+    these types in result sets, as the protocol layer will translate them back
+    to the 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 8dc8d7a0ce..dfe73ca74c 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,440 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   With <firstterm>automatic client-side column-level encryption</firstterm>,
+   columns can be stored encrypted in the database.  The encryption and
+   decryption happens automatically 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 Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   Automatic client-side column-level 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 asymmetric 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 from making
+   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 automatic client-side column-level
+   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 automatic client-side column-level
+       encryption.  Not all client libraries do.  Furthermore, the client
+       library might require that automatic client-side column-level
+       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>
+    would 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 would leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of client-side column-level encryption.
+    (And even ignoring that, it could not work because the server does not
+    have access to the keys to perform the 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>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</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>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   The steps to set up automatic client-side column-level 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"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</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 automatic client-side column-level 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 Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use automatic client-side column-level 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 transport 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 allows 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>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation; for
+    <application>libpq</application>, see <xref
+    linkend="libpq-connect-column-encryption"/>).
+   </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, 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.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <tip>
+    <para>
+     One might be inclined to think of the client-side column-level encryption
+     feature as a mechanism for application writers and users to protect
+     themselves against an <quote>evil DBA</quote>, but that is not the
+     intended purpose.  Rather, it is (also) a tool for the DBA to control
+     which data they do not want (in plaintext) on the server.
+    </para>
+   </tip>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on 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>
 
@@ -1985,6 +2419,14 @@ <title>Privileges</title>
        server.  Grantees may also create, alter, or drop their own user
        mappings associated with that server.
       </para>
+      <para>
+       For column master keys, allows the creation of column encryption keys
+       using the master key.
+      </para>
+      <para>
+       For column encryption keys, allows the use of the key in the creation
+       of table columns.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2151,6 +2593,8 @@ <title>ACL Privilege Abbreviations</title>
       <entry><literal>USAGE</literal></entry>
       <entry><literal>U</literal></entry>
       <entry>
+       <literal>COLUMN ENCRYPTION KEY</literal>,
+       <literal>COLUMN MASTER KEY</literal>,
        <literal>DOMAIN</literal>,
        <literal>FOREIGN DATA WRAPPER</literal>,
        <literal>FOREIGN SERVER</literal>,
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 0cbdf63632..137c0e366f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -23349,6 +23349,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 7c01a541fe..818038d860 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,32 @@ <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 automatic
+     client-side column-level encryption (<xref
+     linkend="ddl-column-encryption"/>).  Column encryption keys are stored in
+     the database encrypted by another key, the <glossterm
+     linkend="glossary-column-master-key">column master key</glossterm>.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt <glossterm
+     linkend="glossary-column-encryption-key">column encryption
+     keys</glossterm>.  (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 0e7ae70c70..d9069e897f 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,141 @@ <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 <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, this enables automatic client-side column-level
+        encryption for the connection.  If encrypted columns are queried and
+        this is not enabled, the encrypted values are 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>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </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>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </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 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 --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2999,32 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            half-byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this half-byte to one to force
+            encryption.  For example, use the C code literal
+            <literal>0x10</literal> to specify text format with forced
+            encryption.  If the array pointer is null then encryption is not
+            forced for any parameter.
+           </para>
+
+           <para>
+            Parameters corresponding to encrypted columns must be passed in
+            text format.  Specifying binary format for such a parameter will
+            result in an error.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter 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 compromised 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.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3037,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3196,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</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 *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> 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>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4084,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>
 
@@ -4059,6 +4287,31 @@ <title>Retrieving Query Result Information</title>
      </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-PQexecPreparedDescribed"/> 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>
 
@@ -4584,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4845,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4902,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4963,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</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 PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> 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-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5052,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8086,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 93fc7167d4..52c29bf8da 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,76 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-column-encryption">
+   <title>Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    Automatic client-side column-level 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 automatic client-side column-level 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 automatic column-level 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, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column-level encryption protocol option is enabled.  That way, a client
+    library only needs to support decrypting data sent in binary and does not
+    have to support decoding the text format of the encryption-related types
+    (see <xref linkend="datatype-encrypted"/>).
+   </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 automatic client-side column-level 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 automatic client-side column-level
+    encryption are described in <xref
+    linkend="protocol-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -4061,6 +4131,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-flow-column-encryption"/>.)
+     </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>Int32</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-flow-column-encryption"/>.)
+     </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>
@@ -5157,6 +5361,45 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), 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>Int32</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>
 
@@ -5545,6 +5788,34 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), 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>Int32</term>
+       <listitem>
+        <para>
+         If the field is encrypted, this specifies the identifier of the
+         encryption algorithm, else zero.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7370,6 +7641,176 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-column-encryption-crypto">
+  <title>Automatic Client-side Column-level Encryption Cryptography</title>
+
+  <para>
+   This section describes the cryptographic operations used by the automatic
+   client-side column-level encryption functionality.  A client that supports
+   this functionality 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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</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>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</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>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</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>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</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>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</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 version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </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 IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </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 54b5f22d6e..a730e5d650 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 0000000000..655e1e00d8
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+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 }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</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
+   schema.  (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 (optionally schema-qualified) 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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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 0000000000..7f0e656ef0
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+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> ( REALM = <replaceable>realm</replaceable> )
+
+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 }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </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 schema.
+   (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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..1caf9bfa56 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..ebcbf5d00a 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,16 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support automatic client-side
+    column-level encryption or decryption; its input or output data will
+    always be the ciphertext.  This is usually suitable for backups (see also
+    <xref linkend="app-pgdump"/>).  If automatic client-side encryption or
+    decryption is wanted, <command>INSERT</command> and
+    <command>SELECT</command> need to be used instead to write and read the
+    data.
+   </para>
+
  </refsect1>
 
  <refsect1>
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 0000000000..56fb253dad
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,170 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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.  You must have <literal>USAGE</literal> privilege on the
+      column master 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>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></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.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </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 0000000000..6aaa1088d1
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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 a03dee4afe..d1549c7f45 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 | DEFAULT } ] [ 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 | DEFAULT } ] [ 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> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,47 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables automatic client-side column-level 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.  You must have <literal>USAGE</literal> privilege
+          on the column encryption key.
+         </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 id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -704,6 +745,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</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 0000000000..f2ac1beb08
--- /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 (optionally schema-qualified) 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 0000000000..fae95e09d1
--- /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 (optionally schema-qualified) 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/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 35bf0332c8..f712f8e9e4 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -46,6 +46,16 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN ENCRYPTION KEY <replaceable>cek_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN MASTER KEY <replaceable>cmk_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON DOMAIN <replaceable>domain_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -513,7 +523,7 @@ <title>Compatibility</title>
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
+    Privileges on databases, tablespaces, schemas, keys, languages, and
     configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 2c938cd7e1..bb2e12148c 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -715,6 +715,48 @@ <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 turns on the column encryption connection option in
+        <application>libpq</application> (see <xref
+        linkend="libpq-connect-column-encryption"/>).  Column master key
+        lookup must be configured by the user, either through a connection
+        option or an environment setting (see <xref
+        linkend="libpq-connect-cmklookup"/>).
+       </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.  (But then it is recommended
+        to not do this on the same host as the server, to avoid exposing
+        unencrypted data that is meant to be kept encrypted on the server.)
+        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 ... \bind ... \g 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 e62d05e5ab..4bf60c729f 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 dc6528dc11..e68b8440be 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4026,6 +4054,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <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 id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..c898997915 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 ef818228ac..a00545080d 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_sendint32(&buf, 0);	/* CEK alg */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 72faeb5dfa..63a3d9b0f9 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,156 @@ 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;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	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_sendint32(&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;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	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 +332,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 +349,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(int32));	/* attencalg */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +369,8 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +402,29 @@ 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->attusertypid;
+			atttypmod = orig_att->attusertypmod;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +432,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_writeint32(buf, attencalg);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +470,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 72a2c3d3db..f86ba299c3 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->attusertypid != attr2->attusertypid)
+			return false;
+		if (attr1->attusertypmod != attr2->attusertypmod)
+			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->attusertypid = 0;
+	att->attusertypmod = -1;
 	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->attusertypid = 0;
+	att->attusertypmod = -1;
 	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 24bab58499..cc48069932 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 a60107bf94..7b9575635b 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 c4232344aa..a3547c6cae 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.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"
@@ -247,6 +249,12 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_SEQUENCE:
 			whole_mask = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			whole_mask = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			whole_mask = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			whole_mask = ACL_ALL_RIGHTS_DATABASE;
 			break;
@@ -473,6 +481,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
 			errormsg = gettext_noop("invalid privilege type %s for sequence");
 			break;
+		case OBJECT_CEK:
+			all_privileges = ACL_ALL_RIGHTS_CEK;
+			errormsg = gettext_noop("invalid privilege type %s for column encryption key");
+			break;
+		case OBJECT_CMK:
+			all_privileges = ACL_ALL_RIGHTS_CMK;
+			errormsg = gettext_noop("invalid privilege type %s for column master key");
+			break;
 		case OBJECT_DATABASE:
 			all_privileges = ACL_ALL_RIGHTS_DATABASE;
 			errormsg = gettext_noop("invalid privilege type %s for database");
@@ -597,6 +613,12 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_SEQUENCE:
 			ExecGrant_Relation(istmt);
 			break;
+		case OBJECT_CEK:
+			ExecGrant_common(istmt, ColumnEncKeyRelationId, ACL_ALL_RIGHTS_CEK, NULL);
+			break;
+		case OBJECT_CMK:
+			ExecGrant_common(istmt, ColumnMasterKeyRelationId, ACL_ALL_RIGHTS_CMK, NULL);
+			break;
 		case OBJECT_DATABASE:
 			ExecGrant_common(istmt, DatabaseRelationId, ACL_ALL_RIGHTS_DATABASE, NULL);
 			break;
@@ -676,6 +698,26 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 				objects = lappend_oid(objects, relOid);
 			}
 			break;
+		case OBJECT_CEK:
+			foreach(cell, objnames)
+			{
+				List	   *cekname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cek_oid(cekname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
+		case OBJECT_CMK:
+			foreach(cell, objnames)
+			{
+				List	   *cmkname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cmk_oid(cmkname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
 		case OBJECT_DATABASE:
 			foreach(cell, objnames)
 			{
@@ -2693,6 +2735,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("permission denied for column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("permission denied for column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -2798,6 +2846,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:
@@ -2828,6 +2877,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;
@@ -2938,6 +2993,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:
@@ -3019,6 +3075,10 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
+		case OBJECT_CEK:
+			return object_aclmask(ColumnEncKeyRelationId, object_oid, roleid, mask, how);
+		case OBJECT_CMK:
+			return object_aclmask(ColumnMasterKeyRelationId, object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
 		case OBJECT_FUNCTION:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..cab6cfd140 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 */
@@ -1493,6 +1499,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 4f006820b8..df282c796f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,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_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,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_attusertypid - 1] = ObjectIdGetDatum(attrs->attusertypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypmod - 1] = Int32GetDatum(attrs->attusertypmod);
 		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)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attusertypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attusertypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 14e57adee2..00f914bc5f 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -26,6 +26,8 @@
 #include "catalog/dependency.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1997,6 +1999,254 @@ OpfamilyIsVisible(Oid opfid)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4567,6 +4817,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(OpfamilyIsVisible(oid));
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2f688166e1..6c4ab9ac59 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,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		Anum_pg_colenckey_cekowner,
+		Anum_pg_colenckey_cekacl,
+		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,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		Anum_pg_colmasterkey_cmkowner,
+		Anum_pg_colmasterkey_cmkacl,
+		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
@@ -1029,6 +1086,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1108,6 +1175,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, 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));
@@ -2311,6 +2393,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2367,6 +2451,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2489,6 +2574,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2575,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3040,6 +3128,92 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				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);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				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 data of %s for %s"),
+								 getObjectDescription(&(ObjectAddress){ColumnEncKeyRelationId, cekdata->ckdcekid}, false),
+								 getObjectDescription(&(ObjectAddress){ColumnMasterKeyRelationId, cekdata->ckdcmkid}, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  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_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4440,6 +4614,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);
@@ -4905,6 +5091,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, 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",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, 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 48f7348f91..69f6175c60 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 bea51b3af1..59c23e9ef8 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"
@@ -118,6 +120,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -379,6 +387,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:
@@ -527,6 +537,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -643,6 +655,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:
@@ -876,6 +891,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 0000000000..3ada6d5aeb
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,439 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, 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/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.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, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	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)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	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")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	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, int 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] = Int32GetDatum(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)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+	nulls[Anum_pg_colenckey_cekacl - 1] = true;
+
+	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 (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(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);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+		AclResult	aclresult;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		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\"",
+						   NameListToString(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)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	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);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+	nulls[Anum_pg_colmasterkey_cmkacl - 1] = true;
+
+	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;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	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)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d6c6d514f3..9685b7886a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 82bda15889..b4c681005a 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +517,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..4c1628cf7b 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 42cced9ebe..4b5ac30441 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..93d61c96ad 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 62d9917ca3..c30b59265b 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"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,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);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,16 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			AclResult	aclresult;
+
+			GetColumnEncryption(colDef->encryption, attr);
+			aclresult = object_aclcheck(ColumnEncKeyRelationId, attr->attcek, GetUserId(), ACL_USAGE);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attr->attcek, false));
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -2562,13 +2575,43 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				Oid			defCollId;
 
 				/*
-				 * Yes, try to merge the two column definitions. They must
-				 * have the same type, typmod, and collation.
+				 * Yes, try to merge the two column definitions.
 				 */
 				ereport(NOTICE,
 						(errmsg("merging multiple inherited definitions of column \"%s\"",
 								attributeName)));
 				def = (ColumnDef *) list_nth(inhSchema, exist_attno - 1);
+
+				/*
+				 * Check encryption parameter.  All parents must have the same
+				 * encryption settings for a column.
+				 */
+				if ((def->encryption && !attribute->attcek) ||
+					(!def->encryption && attribute->attcek))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && attribute->attcek)
+				{
+					/*
+					 * Merging the encryption properties of two encrypted
+					 * parent columns is not yet implemented.  Right now, this
+					 * would confuse the checks of the type etc. below (we
+					 * must check the physical and the real types against each
+					 * other, respectively), which might require a larger
+					 * restructuring.  For now, just give up here.
+					 */
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("multiple inheritance of encrypted columns is not implemented")));
+				}
+
+				/*
+				 * Must have the same type, typmod, and collation.
+				 */
 				typenameTypeIdAndMod(NULL, def->typeName, &defTypeId, &deftypmod);
 				if (defTypeId != attribute->atttypid ||
 					deftypmod != attribute->atttypmod)
@@ -2641,6 +2684,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+														attribute->attusertypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -2919,6 +2968,34 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								 errdetail("%s versus %s", def->compression, newdef->compression)));
 				}
 
+				/*
+				 * Check encryption parameter.  All parents and children must
+				 * have the same encryption settings for a column.
+				 */
+				if ((def->encryption && !newdef->encryption) ||
+					(!def->encryption && newdef->encryption))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && newdef->encryption)
+				{
+					FormData_pg_attribute a, newa;
+
+					GetColumnEncryption(def->encryption, &a);
+					GetColumnEncryption(newdef->encryption, &newa);
+
+					if (a.atttypid != newa.atttypid ||
+						a.atttypmod != newa.atttypmod ||
+						a.attcek != newa.attcek)
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" has an encryption specification conflict",
+										attributeName)));
+				}
+
 				/* Mark the column as locally defined */
 				def->is_local = true;
 				/* Merge of NOT NULL constraints = OR 'em together */
@@ -6861,6 +6938,19 @@ 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);
+		aclresult = object_aclcheck(ColumnEncKeyRelationId, attribute.attcek, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attribute.attcek, false));
+	}
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attusertypid = 0;
+		attribute.attusertypmod = -1;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12692,6 +12782,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:
@@ -19291,3 +19384,110 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	List	   *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 = defGetQualifiedName(el);
+		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);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				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 = get_cek_oid(cek, false);
+
+	attr->attcek = cekoid;
+	attr->attusertypid = attr->atttypid;
+	attr->attusertypmod = attr->atttypmod;
+
+	/* 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;
+
+	attr->atttypmod = alg;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	List	   *result;
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	char	   *nspname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attr->attcek));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attr->attcek);
+
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+	nspname = get_namespace_name(form->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(form->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->atttypmod))),
+									-1));
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index bb0f5de4c2..cec2daa9c3 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/commands/view.c b/src/backend/commands/view.c
index ff98c773f5..171dca3803 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index dc8415a693..c81bc15128 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3983,6 +3983,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/gram.y b/src/backend/parser/gram.y
index a0138382a1..9f4b1e7d94 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,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
@@ -592,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
@@ -690,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
@@ -714,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
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -3700,14 +3705,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;
@@ -3716,8 +3722,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;
@@ -3774,6 +3780,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4045,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6282,33 @@ 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
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (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; }
@@ -6289,6 +6328,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)); }
@@ -6800,6 +6843,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -7611,6 +7656,24 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| COLUMN ENCRYPTION KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CEK;
+					n->objs = $4;
+					$$ = n;
+				}
+			| COLUMN MASTER KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CMK;
+					n->objs = $4;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9140,6 +9203,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9817,6 +9900,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10148,6 +10251,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11304,6 +11425,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_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 any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16958,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16854,6 +17022,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17506,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17425,6 +17595,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 2240284f21..a8a1855aa0 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,159 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			Expr	   *expr = te->expr;
+
+			/*
+			 * If it's a RelabelType, look inside.  (For encrypted columns,
+			 * this would typically be a typmod adjustment.)
+			 */
+			if (IsA(expr, RelabelType))
+				expr = castNode(RelabelType, expr)->arg;
+
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(expr, Param))
+			{
+				Param	   *p = (Param *) expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(expr, Var))
+			{
+				Var		   *v = (Var *) expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell   *lc;
+
+					foreach(lc, srcrte->values_lists)
+					{
+						List	   *values_list = lfirst_node(List, lc);
+						Expr	   *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, RelabelType))
+							value = castNode(RelabelType, value)->arg;
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach(lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f9218f48aa..8d902292cd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1035,8 +1035,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+												attribute->attusertypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2552327d90..79ec9c4d1f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2210,12 +2210,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 cab709b07b..c5acdd0dc2 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -71,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"
 
@@ -1815,6 +1817,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 $%d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2560,6 +2572,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2618,11 +2632,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	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 = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				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 + 1)));
+
+			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->attusertypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->atttypmod;
+			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_sendint32(&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 c7d9d96b45..0a17eb4486 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,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,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 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,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 +2665,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 +2791,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 +3100,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3733,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 8f7522d103..b934f16074 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -800,6 +800,14 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			/* for backwards compatibility, grant some rights by default */
 			world_default = ACL_CREATE_TEMP | ACL_CONNECT;
@@ -911,6 +919,12 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 's':
 			objtype = OBJECT_SEQUENCE;
 			break;
+		case 'Y':
+			objtype = OBJECT_CEK;
+			break;
+		case 'y':
+			objtype = OBJECT_CMK;
+			break;
 		case 'd':
 			objtype = OBJECT_DATABASE;
 			break;
@@ -2915,6 +2929,10 @@ convert_column_priv_string(text *priv_type_text)
 }
 
 
+// TODO: has_column_encryption_key_privilege variants
+// TODO: has_column_master_key_privilege variants
+
+
 /*
  * has_database_privilege variants
  *		These are all named "has_database_privilege" at the SQL level.
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 170b3a3820..faf5a68849 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -679,6 +679,112 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c07382051d..7ad159110f 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
  *
@@ -3683,3 +3705,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+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;
+}
+
+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 77c2ba3f8f..d40af13efe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 94abede512..eb432260da 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"
@@ -219,6 +222,32 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAMENSP] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameNspIndexId,
+		KEY(Anum_pg_colenckey_cekname,
+			Anum_pg_colenckey_ceknamespace),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +262,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAMENSP] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameNspIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname,
+			Anum_pg_colmasterkey_cmknamespace),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 033647011b..4b6be68f27 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"
@@ -130,6 +132,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())
 	{
 		/*
@@ -237,6 +245,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,
@@ -297,7 +311,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 a43f2e5553..e67462c81a 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -201,6 +203,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);
 
@@ -859,6 +867,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 9753a6d868..0a5178bcf1 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -484,6 +484,10 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('U', "USAGE");
 	}
+	else if (strcmp(type, "COLUMN ENCRYPTION KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "COLUMN MASTER KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "DATABASE") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,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 269bfce019..baf0de3347 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3429,6 +3429,8 @@ _getObjectDescription(PQExpBuffer buf, const 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, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 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 1a06eeaf6a..2fd7e51938 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,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);
@@ -393,6 +396,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},
@@ -685,6 +689,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");
 
@@ -1057,6 +1064,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"
@@ -5572,6 +5580,164 @@ 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_ceknamespace;
+	int			i_cekowner;
+	int			i_cekacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, cek.cekowner, cek.cekacl, acldefault('Y', cek.cekowner) AS acldefault\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_ceknamespace = PQfnumber(res, "ceknamespace");
+	i_cekowner = PQfnumber(res, "cekowner");
+	i_cekacl = PQfnumber(res, "cekacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cekacl));
+		cekinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cekinfo[i].dacl.privtype = 0;
+		cekinfo[i].dacl.initprivs = NULL;
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, 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].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			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);
+		if (!PQgetisnull(res, i, i_cekacl))
+			cekinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	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_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+	int			i_cmkacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, cmk.cmkowner, cmk.cmkrealm, cmk.cmkacl, acldefault('y', cmk.cmkowner) AS acldefault\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_cmknamespace = PQfnumber(res, "cmknamespace");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+	i_cmkacl = PQfnumber(res, "cmkacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		cmkinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cmkacl));
+		cmkinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cmkinfo[i].dacl.privtype = 0;
+		cmkinfo[i].dacl.initprivs = NULL;
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cmkacl))
+			cmkinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8187,6 +8353,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8246,8 +8415,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
+	appendPQExpBuffer(q, "SELECT\n"
 						 "a.attrelid,\n"
 						 "a.attnum,\n"
 						 "a.attname,\n"
@@ -8260,7 +8428,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attlen,\n"
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
+						 "pg_catalog.format_type(%s) AS atttypname,\n"
 						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
@@ -8269,7 +8437,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "' ' || pg_catalog.quote_literal(option_value) "
 						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
 						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+						 "), E',\n    ') AS attfdwoptions,\n",
+						 fout->remoteVersion >= 160000 ?
+						 "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END, CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END" :
+						 "a.atttypid, a.atttypmod");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -8295,10 +8466,23 @@ 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,
+						  "a.attcek,\n"
+						  "CASE a.atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "CASE WHEN a.atttypid IN (%u, %u) THEN a.atttypmod END AS attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID,
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcek,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
@@ -8323,6 +8507,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	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");
@@ -8383,6 +8570,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->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		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));
@@ -8410,6 +8600,22 @@ 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_attcek))
+			{
+				Oid		attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[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));
@@ -9928,6 +10134,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;
@@ -13321,6 +13533,141 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * 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",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_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,
+								  .namespace = cekinfo->dobj.namespace->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,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cekinfo->dobj.dumpId, InvalidDumpId, "COLUMN ENCRYPTION KEY",
+				qcekname, NULL, cekinfo->dobj.namespace->dobj.name,
+				cekinfo->rolname, &cekinfo->dacl);
+
+	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",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	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,
+								  .namespace = cmkinfo->dobj.namespace->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,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cmkinfo->dobj.dumpId, InvalidDumpId, "COLUMN MASTER KEY",
+				qcmkname, NULL, cmkinfo->dobj.namespace->dobj.name,
+				cmkinfo->rolname, &cmkinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15404,6 +15751,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[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_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -17967,6 +18330,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 cdca0b993d..d4a2e595d0 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,
@@ -332,6 +334,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	struct _CekInfo **attcek;
+	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 */
@@ -663,6 +668,32 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	struct _CmkInfo **cekcmks;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -683,6 +714,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -710,6 +743,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 8266c117a3..d3dacd39da 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 cd421c5944..6530fb81a2 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 d92247c915..f4f13fd087 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -645,6 +645,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, 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, },
@@ -1245,6 +1257,26 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1663,6 +1695,26 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3496,6 +3548,26 @@
 		unlike => { no_privs => 1, },
 	},
 
+	'GRANT USAGE ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
+	'GRANT USAGE ON COLUMN MASTER KEY cmk1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
 	'GRANT USAGE ON FOREIGN DATA WRAPPER dummy' => {
 		create_order => 85,
 		create_sql   => 'GRANT USAGE ON FOREIGN DATA WRAPPER dummy
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..0d6a46d24b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c8a0bb7b3a..04d437f836 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1530,7 +1530,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;
@@ -1546,6 +1546,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;
@@ -1568,6 +1569,8 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attusertypid;
+	const char *attusertypmod;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1844,7 +1847,17 @@ 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)");
+	if (pset.sversion >= 160000)
+	{
+		attusertypid = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END";
+		attusertypmod = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END";
+	}
+	else
+	{
+		attusertypid = "a.atttypid";
+		attusertypmod = "a.atttypmod";
+	}
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, %s)", attusertypid, attusertypmod);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1857,7 +1870,8 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		attrdef_col = cols++;
 		attnotnull_col = cols++;
-		appendPQExpBufferStr(&buf, ",\n  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
+		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");
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
@@ -1909,6 +1923,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			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 ||
@@ -2032,6 +2058,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)
@@ -2124,6 +2152,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),
@@ -4477,6 +4516,152 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cekacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cmkacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cmk.oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 554fe86725..1cf8f72176 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1729966959 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,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 73d4b393bc..010bc5a6d5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,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 5a28b6f713..6736505c3a 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 5e1882eaea..72add64773 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -933,6 +933,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1223,6 +1237,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
@@ -1705,7 +1721,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1952,6 +1968,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_SCHEMA_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", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2894,6 +2926,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",
@@ -3619,6 +3671,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) ||
@@ -3931,6 +3984,8 @@ psql_completion(const char *text, int start, int end)
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"COLUMN ENCRYPTION KEY",
+											"COLUMN MASTER KEY",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4046,6 +4101,16 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+	/* Complete "GRANT/REVOKE * ON COLUMN ENCRYPTION|MASTER KEY *" with TO/FROM */
+	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
+			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny))
+	{
+		if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny, MatchAny))
+			COMPLETE_WITH("TO");
+		else
+			COMPLETE_WITH("FROM");
+	}
+
 	/* Complete "GRANT/REVOKE * ON FOREIGN DATA WRAPPER *" with TO/FROM */
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny))
diff --git a/src/common/Makefile b/src/common/Makefile
index 113029bf7b..73dce1150e 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		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";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index 41bd58ebdf..3695d3285b 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -4,6 +4,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 747ecb800d..d199033651 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 ffd5e9dc82..eb59e73c0a 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/heap.h b/src/include/catalog/heap.h
index d01ab504b6..758696b539 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 3179be09d3..9e2c5256f7 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,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/namespace.h b/src/include/catalog/namespace.h
index f64a0ec26b..d0b9e8458d 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -115,6 +115,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c4d6adcd3e..c58b79e3a7 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 5b950129de..0e9e85ebf3 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 b561e17781..7910175a6a 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,17 @@ 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);
+
+	/*
+	 * User-visible type and typmod, currently used for encrypted columns.
+	 * These are only set to nondefault values if they are different from
+	 * atttypid and attypmod.
+	 */
+	Oid			attusertypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+	int32		attusertypmod BKI_DEFAULT(-1);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..c57fa18a27
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		cekacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_TOAST(pg_colenckey, 8263, 8264);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_nsp_index, 8242, ColumnEncKeyNameNspIndexId, on pg_colenckey using btree(cekname name_ops, ceknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /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-2023, 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);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..d3bfd36279
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+	aclitem		cmkacl[1] BKI_DEFAULT(_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_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, on pg_colmasterkey using btree(cmkname name_ops, cmknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c867d99563..ff06d52fd0 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 b2cdea66c4..114279fa64 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 91587b99d0..c21052a3f7 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 e2a7642a2b..0d6e23c9c9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6355,6 +6355,14 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -11935,4 +11943,37 @@
   proname => 'any_value_transfn', prorettype => 'anyelement',
   proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_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 92bcaf2c73..85e4b5554e 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 => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 519e570c8c..3c7ab2a8fe 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 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, 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);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e7c2b91a58..11f88c59ce 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,4 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml>.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index ac6407e9f6..39a286e58a 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 f7d7f10f7d..4e2256f2c5 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -88,8 +88,7 @@ typedef uint64 AclMode;			/* a bitmask of privilege bits */
 #define ACL_REFERENCES	(1<<5)
 #define ACL_TRIGGER		(1<<6)
 #define ACL_EXECUTE		(1<<7)	/* for functions */
-#define ACL_USAGE		(1<<8)	/* for languages, namespaces, FDWs, and
-								 * servers */
+#define ACL_USAGE		(1<<8)	/* for various object types */
 #define ACL_CREATE		(1<<9)	/* for namespaces and databases */
 #define ACL_CREATE_TEMP (1<<10) /* for databases */
 #define ACL_CONNECT		(1<<11) /* for databases */
@@ -722,6 +721,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? */
@@ -758,11 +758,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1980,6 +1981,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2167,6 +2171,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..c8cf9c7776 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_param.h b/src/include/parser/parse_param.h
index d4865e50f6..26e1cd617a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..c1f6fe1410 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/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2..0c73022833 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -159,6 +159,8 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
+#define ACL_ALL_RIGHTS_CEK			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_CMK			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..1dac24e4b4 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,9 @@ 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 char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, 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 a443181d41..8ecacb29df 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d5d50ceab4..bf1d527c1a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAMENSP,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAMENSP,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c18e914228..10dfda8016 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 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
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 50b5df3490..bf6149e859 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}
@@ -1412,6 +1420,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4029,6 +4059,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 0000000000..ab6c70967f
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,842 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, 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 "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = 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;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								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);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05>
+ * for encrypting and decrypting column values.
+ */
+
+#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,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+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;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+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;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+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,
+					 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;
+	int64		al;
+	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;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	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;
+}
+
+/*
+ * Decrypt a column value
+ */
+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			iv_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 = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_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;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_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;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#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)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "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 */
+
+/*
+ * Encrypt a column value
+ */
+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			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_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)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	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, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								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,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, 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, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, 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, const char *cmkfilename, 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 ec62550e38..7c7e2bac12 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,414 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+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 *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					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, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', 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)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, "r");
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+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
@@ -1251,13 +1662,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				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;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				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;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* 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;
@@ -1500,6 +1949,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1967,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2006,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2122,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2165,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2264,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2313,47 @@ PQsendQueryGuts(PGconn *conn,
 		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;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2372,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2390,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#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)
+				{
+					libpq_append_conn_error(conn, "column encryption key not found");
+					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
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2872,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4140,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4188,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)
 {
@@ -3762,6 +4384,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 8ab6a88416..3e3d8674be 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);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -547,6 +555,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) ||
@@ -561,6 +571,21 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, 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.
@@ -582,8 +607,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;
 	}
 
@@ -685,10 +712,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, 4, 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! */
@@ -1468,6 +1516,92 @@ 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);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	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;
+	int			ret;
+
+	/* 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, 4, 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 */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2420,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 abaab6a073..77791e8349 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);
+			pqTraceOutputInt32(f, message, cursor, false);
+			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);
+			pqTraceOutputInt32(f, message, cursor, false);
+		}
 	}
 }
 
@@ -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);
+	pqTraceOutputInt32(f, message, cursor, false);
+	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 f3d9220496..cf339a3e53 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,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,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 +559,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 d7ec5ed429..cbfaa7f95f 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);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 573fd9b6ea..a6e6b2ada9 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,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
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': get_option('ssl')},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 4df544ecef..0c36aa5f32 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-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_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext pqInternalNotice:2
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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 b2a4b06fd2..87d2808b52 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c8ba170503 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 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..d5ead874e5
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, 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
+
+export OPENSSL PERL
+
+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 0000000000..47f88c41ce
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,24 @@
+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',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+      'PERL': perl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..c56af737ac
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,255 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+my $perl = $ENV{PERL};
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$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"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	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}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# 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\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[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 %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = "*=run:$perl ./test_run_decrypt.pl %k %a %p";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$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\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$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 update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$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', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+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');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$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 tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$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 second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..14eafb8ec9
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, 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 $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# 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, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+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, algorithm = '$cmkalg', 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 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	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;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	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 = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	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
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/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-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $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}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..d55ddb1ab7 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,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 0000000000..193ccacfff
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,353 @@
+\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;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+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       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  permission denied for column master key cmk1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+ERROR:  permission denied for column encryption key cek1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column 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 in schema "public"
+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 in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION '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 SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf..77fedce8aa 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -107,7 +109,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
@@ -327,6 +330,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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+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},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 -- these object types cannot be qualified names
 SELECT pg_get_object_address('language', '{one}', '{}');
 ERROR:  language "one" does not exist
@@ -409,6 +430,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}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,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
@@ -547,6 +576,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
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|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 215eb899be..226f5e404e 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 {attusertypid} => 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,9 @@ 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 {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {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 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ 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)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,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
@@ -872,6 +876,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
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 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,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 15e015b3d6..577a6d4b2e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 427429975e..57c7d2bec3 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..6e63e6d4f5
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,265 @@
+\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;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+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
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+
+
+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 SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..35af43032f 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -99,7 +101,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
@@ -174,6 +177,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}', '{}'),
@@ -228,6 +234,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;
 
@@ -247,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
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 2ddab010c2777c6a965cea82dc1b809ddc33ecc1
-- 
2.39.2

#68Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Mark Dilger (#65)
Re: Transparent column encryption

On 11.02.23 22:54, Mark Dilger wrote:

Thanks Peter. Here are some observations about the documentation in patch version 15.

In acronyms.sgml, the CEK and CMK entries should link to documentation, perhaps linkend="glossary-column-encryption-key" and linkend="glossary-column-master-key". These glossary entries should in turn link to linkend="ddl-column-encryption".

In ddl.sgml, the sentence "forcing encryption of certain parameters in the client library (see its documentation)" should link to linkend="libpq-connect-column-encryption".

Did you intend the use of "transparent data encryption" (rather than "transparent column encryption") in datatype.sgml? If so, what's the difference?

There are all addressed in the v16 patch I just posted.

Is this feature intended to be available from ecpg? If so, can we maybe include an example in 36.3.4. Prepared Statements showing how to pass the encrypted values securely. If not, can we include verbiage about that limitation, so folks don't waste time trying to figure out how to do it?

It should "just work". I will give this a try sometime, but I don't see
why it wouldn't work.

The documentation for pg_dump (and pg_dumpall) now includes a --decrypt-encrypted-columns option, which I suppose requires cmklookup to first be configured, and for PGCMKLOOKUP to be exported. There isn't anything in the pg_dump docs about this, though, so maybe a link to section 5.5.3 with a warning about not running pg_dump this way on the database server itself?

Also addressed in v16.

How does a psql user mark a parameter as having forced encryption? A libpq user can specify this in the paramFormats array, but I don't see any syntax for doing this from psql. None of the perl tap tests you've included appear to do this (except indirectly when calling test_client); grep'ing for the libpq error message "parameter with forced encryption is not to be encrypted" in the tests has no matches. Is it just not possible? I thought you'd mentioned some syntax for this when we spoke in person, but I don't see it now.

This has been asked about before. We just need to come up with a syntax
for it. The issue is contained inside psql.

#69Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Mark Dilger (#66)
Re: Transparent column encryption

On 12.02.23 15:48, Mark Dilger wrote:

I should mention, src/sgml/html/libpq-exec.html needs clarification:

paramFormats[]Specifies whether parameters are text (put a zero in the array entry for the corresponding parameter) or binary (put a one in the array entry for the corresponding parameter). If the array pointer is null then all parameters are presumed to be text strings.

Perhaps you should edit this last sentence to say that all parameters are presumed to be test strings without forced encryption.

This is actually already mentioned later in that section.

When column encryption is enabled, the second-least-significant byte of this parameter specifies whether encryption should be forced for a parameter.

The value 0x10 has a one as its second-least-significant *nibble*, but that's a really strange way to describe the high-order nibble, and perhaps not what you mean. Could you clarify?

Set this byte to one to force encryption.

I think setting the byte to one (0x01) means "binary unencrypted", not "force encryption". Don't you mean to set this byte to 16?

For example, use the C code literal 0x10 to specify text format with forced encryption. If the array pointer is null then encryption is not forced for any parameter.
If encryption is forced for a parameter but the parameter 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 compromised 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.)

This was me being confused. I adjusted the text to use "half-byte".

I think you should say something about how specifying 0x11 will behave -- in other words, asking for encrypted binary data. I believe that this is will draw a "format must be text for encrypted parameter" error, and that the docs should clearly say so.

done

#70Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#67)
1 attachment(s)
Re: Transparent column encryption

On 22.02.23 11:25, Peter Eisentraut wrote:

Other changes since v15:

- CEKs and CMKs now have USAGE privileges.  (There are some TODO markers
where I got too bored with boilerplate.  I will fill those in, but the
idea should be clear.)

New patch. The above is all filled in now.

I also figured we need support in the DISCARD command to clear the
session state of what keys have already been sent, for the benefit of
connection poolers, so I added an option there.

The only thing left on my list for this whole thing is some syntax in
psql to force encryption for a parameter. But that could also be done
as a separate patch.

Attachments:

v17-0001-Automatic-client-side-column-level-encryption.patchtext/plain; charset=UTF-8; name=v17-0001-Automatic-client-side-column-level-encryption.patchDownload
From 65eef95ef3a89178378d36fcd65a67c0d1592693 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Tue, 28 Feb 2023 21:23:25 +0100
Subject: [PATCH v17] Automatic client-side column-level encryption

This feature enables the automatic 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 asymmetric 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
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.

To get automatically encrypted data into the database (as opposed to
reading it out), it is required to use protocol-level prepared
statements (i.e., extended query).  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.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this if the
\bind command is used.

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.

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                    | 317 +++++++
 doc/src/sgml/charset.sgml                     |  10 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 444 +++++++++
 doc/src/sgml/func.sgml                        |  60 ++
 doc/src/sgml/glossary.sgml                    |  26 +
 doc/src/sgml/libpq.sgml                       | 322 +++++++
 doc/src/sgml/protocol.sgml                    | 467 ++++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |  10 +
 .../ref/create_column_encryption_key.sgml     | 173 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  55 +-
 doc/src/sgml/ref/discard.sgml                 |  14 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/grant.sgml                   |  12 +-
 doc/src/sgml/ref/pg_dump.sgml                 |  42 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   8 +
 src/backend/access/common/printtup.c          | 237 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  60 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 288 ++++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  17 +
 src/backend/commands/colenccmds.c             | 439 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/discard.c                |   8 +-
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 204 ++++-
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 200 ++++-
 src/backend/parser/parse_param.c              | 157 ++++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  56 ++
 src/backend/utils/adt/acl.c                   | 398 +++++++++
 src/backend/utils/adt/varlena.c               | 107 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  42 +
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/dumputils.c                   |   4 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 377 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  35 +
 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              |  72 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 191 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  69 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   4 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |  11 +
 src/include/catalog/pg_colenckey.h            |  46 +
 src/include/catalog/pg_colenckeydata.h        |  46 +
 src/include/catalog/pg_colmasterkey.h         |  47 +
 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               | 103 +++
 src/include/catalog/pg_type.dat               |  13 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  44 +-
 src/include/parser/kwlist.h                   |   3 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   7 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 839 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 671 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 157 +++-
 src/interfaces/libpq/fe-trace.c               |  55 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   2 +-
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  24 +
 .../t/001_column_encryption.pl                | 255 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 451 ++++++++++
 src/test/regress/expected/object_address.out  |  37 +-
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 297 +++++++
 src/test/regress/sql/object_address.sql       |  13 +-
 src/test/regress/sql/type_sanity.sql          |   2 +
 144 files changed, 10399 insertions(+), 91 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/common/colenc.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/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 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 2df6559acc..3a5f2c254c 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; see <xref linkend="ddl-column-encryption"/>
+     </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; see <xref linkend="ddl-column-encryption"/>
+     </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 c1e4048054..808f29669d 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,44 @@ <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>attusertypid</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.  If the column is not
+       encrypted, then 0.  For encrypted columns, the field
+       <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.
+       </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type
+       modifier (analogous to <structfield>atttypmod</structfield>) that is
+       reported to the client.  If the column is not encrypted, then -1.  For
+       encrypted columns, the field <structfield>atttypmod</structfield>)
+       contains 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>
@@ -2467,6 +2520,270 @@ <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>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </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>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </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>int4</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>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </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>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml
index 3032392b80..f3026fff83 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,16 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When automatic client-side column-level encryption is used, then no
+     encoding conversion is possible.  (The encoding conversion happens on the
+     server, and the server cannot look inside any encrypted column values.)
+     If automatic client-side column-level encryption is enabled for a
+     session, then the server enforces that the client encoding matches the
+     server encoding, and any attempts to change the client encoding will be
+     rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..67fce16872 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <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
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support automatic client-side column-level encryption
+    or have disabled it will see the encrypted values in this format.  Clients
+    that support automatic client-side column-level encryption will not see
+    these types in result sets, as the protocol layer will translate them back
+    to the 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 5179125510..65514e119f 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,440 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   With <firstterm>automatic client-side column-level encryption</firstterm>,
+   columns can be stored encrypted in the database.  The encryption and
+   decryption happens automatically 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 Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   Automatic client-side column-level 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 asymmetric 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 from making
+   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 automatic client-side column-level
+   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 automatic client-side column-level
+       encryption.  Not all client libraries do.  Furthermore, the client
+       library might require that automatic client-side column-level
+       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>
+    would 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 employees (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This would leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of client-side column-level encryption.
+    (And even ignoring that, it could not work because the server does not
+    have access to the keys to perform the 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>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</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>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   The steps to set up automatic client-side column-level 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"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</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, algorithm = 'RSAES_OAEP_SHA_1', 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 automatic client-side column-level 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 Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use automatic client-side column-level 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 transport 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 allows 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>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation; for
+    <application>libpq</application>, see <xref
+    linkend="libpq-connect-column-encryption"/>).
+   </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, 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.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <tip>
+    <para>
+     One might be inclined to think of the client-side column-level encryption
+     feature as a mechanism for application writers and users to protect
+     themselves against an <quote>evil DBA</quote>, but that is not the
+     intended purpose.  Rather, it is (also) a tool for the DBA to control
+     which data they do not want (in plaintext) on the server.
+    </para>
+   </tip>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on 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>
 
@@ -1986,6 +2420,14 @@ <title>Privileges</title>
        server.  Grantees may also create, alter, or drop their own user
        mappings associated with that server.
       </para>
+      <para>
+       For column master keys, allows the creation of column encryption keys
+       using the master key.
+      </para>
+      <para>
+       For column encryption keys, allows the use of the key in the creation
+       of table columns.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2152,6 +2594,8 @@ <title>ACL Privilege Abbreviations</title>
       <entry><literal>USAGE</literal></entry>
       <entry><literal>U</literal></entry>
       <entry>
+       <literal>COLUMN ENCRYPTION KEY</literal>,
+       <literal>COLUMN MASTER KEY</literal>,
        <literal>DOMAIN</literal>,
        <literal>FOREIGN DATA WRAPPER</literal>,
        <literal>FOREIGN SERVER</literal>,
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 97b3f1c1a6..dfaa6ff6d8 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22859,6 +22859,40 @@ <title>Access Privilege Inquiry Functions</title>
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_encryption_key_privilege</primary>
+        </indexterm>
+        <function>has_column_encryption_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cek</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column encryption key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_master_key_privilege</primary>
+        </indexterm>
+        <function>has_column_master_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cmk</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column master key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
@@ -23349,6 +23383,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 7c01a541fe..818038d860 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,32 @@ <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 automatic
+     client-side column-level encryption (<xref
+     linkend="ddl-column-encryption"/>).  Column encryption keys are stored in
+     the database encrypted by another key, the <glossterm
+     linkend="glossary-column-master-key">column master key</glossterm>.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt <glossterm
+     linkend="glossary-column-encryption-key">column encryption
+     keys</glossterm>.  (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 3ccd8ff942..26cab10104 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,141 @@ <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 <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, this enables automatic client-side column-level
+        encryption for the connection.  If encrypted columns are queried and
+        this is not enabled, the encrypted values are 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>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </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>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </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 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 --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2999,32 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            half-byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this half-byte to one to force
+            encryption.  For example, use the C code literal
+            <literal>0x10</literal> to specify text format with forced
+            encryption.  If the array pointer is null then encryption is not
+            forced for any parameter.
+           </para>
+
+           <para>
+            Parameters corresponding to encrypted columns must be passed in
+            text format.  Specifying binary format for such a parameter will
+            result in an error.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter 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 compromised 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.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3037,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3196,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</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 *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> 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>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4084,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>
 
@@ -4059,6 +4287,31 @@ <title>Retrieving Query Result Information</title>
      </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-PQexecPreparedDescribed"/> 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>
 
@@ -4584,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4845,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4902,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4963,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</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 PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> 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-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5052,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8086,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 73b7f4432f..c43c3051c3 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,76 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-column-encryption">
+   <title>Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    Automatic client-side column-level 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 automatic client-side column-level 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 automatic column-level 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, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column-level encryption protocol option is enabled.  That way, a client
+    library only needs to support decrypting data sent in binary and does not
+    have to support decoding the text format of the encryption-related types
+    (see <xref linkend="datatype-encrypted"/>).
+   </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 automatic client-side column-level 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 automatic client-side column-level
+    encryption are described in <xref
+    linkend="protocol-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -3841,6 +3911,16 @@ <title>Message Formats</title>
          The parameter format codes.  Each must presently be
          zero (text) or one (binary).
         </para>
+
+        <para>
+         If the protocol extension <literal>_pq_.column_encryption</literal>
+         is enabled (see <xref linkend="protocol-flow-column-encryption"/>),
+         then the second-least-significant half-byte is set to one if the
+         parameter was encrypted by the client.  (So, for example, to send an
+         encrypted value in binary, the field is set to 0x11 in total.)  This
+         is used by the server to check that a parameter that was required to
+         be encrypted was actually encrypted.
+        </para>
        </listitem>
       </varlistentry>
 
@@ -4061,6 +4141,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-flow-column-encryption"/>.)
+     </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>Int32</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-flow-column-encryption"/>.)
+     </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>
@@ -5164,6 +5378,45 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), 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>Int32</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 parameter is to be
+         encrypted and bit 0x0001 is set, the column underlying the parameter
+         uses deterministic encryption, otherwise randomized encryption.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5552,6 +5805,50 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), 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>Int32</term>
+       <listitem>
+        <para>
+         If the field is 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 field is encrypted and
+         bit 0x0001 is set, the field uses deterministic encryption, otherwise
+         randomized encryption.
+        </para>
+        <!--
+            This is not really useful here, but it keeps alignment with
+            ParameterDescription.  Future flags might be useful in both
+            places.
+        -->
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7377,6 +7674,176 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-column-encryption-crypto">
+  <title>Automatic Client-side Column-level Encryption Cryptography</title>
+
+  <para>
+   This section describes the cryptographic operations used by the automatic
+   client-side column-level encryption functionality.  A client that supports
+   this functionality 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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</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>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</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>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</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>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</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>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</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 version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </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 IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </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 54b5f22d6e..a730e5d650 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 0000000000..655e1e00d8
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+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 }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</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
+   schema.  (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 (optionally schema-qualified) 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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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 0000000000..7f0e656ef0
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+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> ( REALM = <replaceable>realm</replaceable> )
+
+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 }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </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 schema.
+   (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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..1caf9bfa56 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..ebcbf5d00a 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,16 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support automatic client-side
+    column-level encryption or decryption; its input or output data will
+    always be the ciphertext.  This is usually suitable for backups (see also
+    <xref linkend="app-pgdump"/>).  If automatic client-side encryption or
+    decryption is wanted, <command>INSERT</command> and
+    <command>SELECT</command> need to be used instead to write and read the
+    data.
+   </para>
+
  </refsect1>
 
  <refsect1>
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 0000000000..65534fb03f
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,173 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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.  You must have <literal>USAGE</literal> privilege on the
+      column master 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>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></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.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </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,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    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,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    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 0000000000..6aaa1088d1
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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 a03dee4afe..d1549c7f45 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 | DEFAULT } ] [ 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 | DEFAULT } ] [ 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> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,47 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables automatic client-side column-level 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.  You must have <literal>USAGE</literal> privilege
+          on the column encryption key.
+         </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 id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -704,6 +745,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</literal></term>
         <listitem>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523ca..6a94706ef7 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@
 
  <refsynopsisdiv>
 <synopsis>
-DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
+DISCARD { ALL | COLUMN ENCRYPTION KEYS | PLANS | SEQUENCES | TEMPORARY | TEMP }
 </synopsis>
  </refsynopsisdiv>
 
@@ -42,6 +42,17 @@ <title>Parameters</title>
 
   <variablelist>
 
+   <varlistentry>
+    <term><literal>COLUMN ENCRYPTION KEYS</literal></term>
+    <listitem>
+     <para>
+      Discards knowledge about which column encryption keys and column master
+      keys have been sent to the client in this session.  (They will
+      subsequently be re-sent as required.)
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>PLANS</literal></term>
     <listitem>
@@ -93,6 +104,7 @@ <title>Parameters</title>
 DISCARD PLANS;
 DISCARD TEMP;
 DISCARD SEQUENCES;
+DISCARD COLUMN ENCRYPTION KEYS;
 </programlisting></para>
     </listitem>
    </varlistentry>
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 0000000000..f2ac1beb08
--- /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 (optionally schema-qualified) 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 0000000000..fae95e09d1
--- /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 (optionally schema-qualified) 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/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 35bf0332c8..f712f8e9e4 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -46,6 +46,16 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN ENCRYPTION KEY <replaceable>cek_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN MASTER KEY <replaceable>cmk_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON DOMAIN <replaceable>domain_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -513,7 +523,7 @@ <title>Compatibility</title>
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
+    Privileges on databases, tablespaces, schemas, keys, languages, and
     configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 49d218905f..536f10def0 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -716,6 +716,48 @@ <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 turns on the column encryption connection option in
+        <application>libpq</application> (see <xref
+        linkend="libpq-connect-column-encryption"/>).  Column master key
+        lookup must be configured by the user, either through a connection
+        option or an environment setting (see <xref
+        linkend="libpq-connect-cmklookup"/>).
+       </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.  (But then it is recommended
+        to not do this on the same host as the server, to avoid exposing
+        unencrypted data that is meant to be kept encrypted on the server.)
+        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 ... \bind ... \g 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 e62d05e5ab..4bf60c729f 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 dc6528dc11..e68b8440be 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4026,6 +4054,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <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 id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..c898997915 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 ef818228ac..c5894893eb 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,12 @@ 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_sendint32(&buf, 0);	/* CEK alg */
+			pq_sendint16(&buf, 0);	/* flags */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 72faeb5dfa..2d627b6f47 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,166 @@ 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;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	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_sendint32(&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;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	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);
+}
+
+void
+DiscardColumnEncryptionKeys(void)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -167,6 +342,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 +359,18 @@ 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(int32)		/* attencalg */
+			   + sizeof(int16));	/* flags */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +380,9 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
+		int16		flags = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +414,31 @@ 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->attusertypid;
+			atttypmod = orig_att->attusertypmod;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->atttypmod;
+			if (orig_att->atttypid == PG_ENCRYPTED_DETOID)
+				flags |= 0x0001;
+			ReleaseSysCache(tp);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +446,12 @@ 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_writeint32(buf, attencalg);
+			pq_writeint16(buf, flags);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +485,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 72a2c3d3db..f86ba299c3 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->attusertypid != attr2->attusertypid)
+			return false;
+		if (attr1->attusertypmod != attr2->attusertypmod)
+			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->attusertypid = 0;
+	att->attusertypmod = -1;
 	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->attusertypid = 0;
+	att->attusertypmod = -1;
 	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 24bab58499..cc48069932 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 a60107bf94..7b9575635b 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 c4232344aa..a3547c6cae 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.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"
@@ -247,6 +249,12 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_SEQUENCE:
 			whole_mask = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			whole_mask = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			whole_mask = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			whole_mask = ACL_ALL_RIGHTS_DATABASE;
 			break;
@@ -473,6 +481,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
 			errormsg = gettext_noop("invalid privilege type %s for sequence");
 			break;
+		case OBJECT_CEK:
+			all_privileges = ACL_ALL_RIGHTS_CEK;
+			errormsg = gettext_noop("invalid privilege type %s for column encryption key");
+			break;
+		case OBJECT_CMK:
+			all_privileges = ACL_ALL_RIGHTS_CMK;
+			errormsg = gettext_noop("invalid privilege type %s for column master key");
+			break;
 		case OBJECT_DATABASE:
 			all_privileges = ACL_ALL_RIGHTS_DATABASE;
 			errormsg = gettext_noop("invalid privilege type %s for database");
@@ -597,6 +613,12 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_SEQUENCE:
 			ExecGrant_Relation(istmt);
 			break;
+		case OBJECT_CEK:
+			ExecGrant_common(istmt, ColumnEncKeyRelationId, ACL_ALL_RIGHTS_CEK, NULL);
+			break;
+		case OBJECT_CMK:
+			ExecGrant_common(istmt, ColumnMasterKeyRelationId, ACL_ALL_RIGHTS_CMK, NULL);
+			break;
 		case OBJECT_DATABASE:
 			ExecGrant_common(istmt, DatabaseRelationId, ACL_ALL_RIGHTS_DATABASE, NULL);
 			break;
@@ -676,6 +698,26 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 				objects = lappend_oid(objects, relOid);
 			}
 			break;
+		case OBJECT_CEK:
+			foreach(cell, objnames)
+			{
+				List	   *cekname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cek_oid(cekname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
+		case OBJECT_CMK:
+			foreach(cell, objnames)
+			{
+				List	   *cmkname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cmk_oid(cmkname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
 		case OBJECT_DATABASE:
 			foreach(cell, objnames)
 			{
@@ -2693,6 +2735,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("permission denied for column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("permission denied for column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -2798,6 +2846,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:
@@ -2828,6 +2877,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;
@@ -2938,6 +2993,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:
@@ -3019,6 +3075,10 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
+		case OBJECT_CEK:
+			return object_aclmask(ColumnEncKeyRelationId, object_oid, roleid, mask, how);
+		case OBJECT_CMK:
+			return object_aclmask(ColumnMasterKeyRelationId, object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
 		case OBJECT_FUNCTION:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..cab6cfd140 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 */
@@ -1493,6 +1499,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 4f006820b8..df282c796f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,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_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,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_attusertypid - 1] = ObjectIdGetDatum(attrs->attusertypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypmod - 1] = Int32GetDatum(attrs->attusertypmod);
 		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)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attusertypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attusertypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 14e57adee2..00f914bc5f 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -26,6 +26,8 @@
 #include "catalog/dependency.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1997,6 +1999,254 @@ OpfamilyIsVisible(Oid opfid)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4567,6 +4817,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(OpfamilyIsVisible(oid));
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2f688166e1..6c4ab9ac59 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,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		Anum_pg_colenckey_cekowner,
+		Anum_pg_colenckey_cekacl,
+		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,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		Anum_pg_colmasterkey_cmkowner,
+		Anum_pg_colmasterkey_cmkacl,
+		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
@@ -1029,6 +1086,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1108,6 +1175,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, 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));
@@ -2311,6 +2393,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2367,6 +2451,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2489,6 +2574,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2575,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3040,6 +3128,92 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				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);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				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 data of %s for %s"),
+								 getObjectDescription(&(ObjectAddress){ColumnEncKeyRelationId, cekdata->ckdcekid}, false),
+								 getObjectDescription(&(ObjectAddress){ColumnMasterKeyRelationId, cekdata->ckdcmkid}, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  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_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4440,6 +4614,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);
@@ -4905,6 +5091,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, 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",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, 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 48f7348f91..69f6175c60 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 bea51b3af1..59c23e9ef8 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"
@@ -118,6 +120,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -379,6 +387,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:
@@ -527,6 +537,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -643,6 +655,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:
@@ -876,6 +891,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 0000000000..3ada6d5aeb
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,439 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, 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/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.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, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	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)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	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")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	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, int 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] = Int32GetDatum(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)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+	nulls[Anum_pg_colenckey_cekacl - 1] = true;
+
+	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 (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(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);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+		AclResult	aclresult;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		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\"",
+						   NameListToString(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)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	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);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+	nulls[Anum_pg_colmasterkey_cmkacl - 1] = true;
+
+	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;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	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)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d6c6d514f3..9685b7886a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 296dc82d2e..86d22ca065 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/printtup.h"
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "commands/async.h"
@@ -25,7 +26,7 @@
 static void DiscardAll(bool isTopLevel);
 
 /*
- * DISCARD { ALL | SEQUENCES | TEMP | PLANS }
+ * DISCARD
  */
 void
 DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
@@ -36,6 +37,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			DiscardAll(isTopLevel);
 			break;
 
+		case DISCARD_COLUMN_ENCRYPTION_KEYS:
+			DiscardColumnEncryptionKeys();
+			break;
+
 		case DISCARD_PLANS:
 			ResetPlanCache();
 			break;
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	DiscardColumnEncryptionKeys();
 }
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 82bda15889..b4c681005a 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +517,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..4c1628cf7b 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 42cced9ebe..4b5ac30441 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..93d61c96ad 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 62d9917ca3..c30b59265b 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"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,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);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,16 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			AclResult	aclresult;
+
+			GetColumnEncryption(colDef->encryption, attr);
+			aclresult = object_aclcheck(ColumnEncKeyRelationId, attr->attcek, GetUserId(), ACL_USAGE);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attr->attcek, false));
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -2562,13 +2575,43 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				Oid			defCollId;
 
 				/*
-				 * Yes, try to merge the two column definitions. They must
-				 * have the same type, typmod, and collation.
+				 * Yes, try to merge the two column definitions.
 				 */
 				ereport(NOTICE,
 						(errmsg("merging multiple inherited definitions of column \"%s\"",
 								attributeName)));
 				def = (ColumnDef *) list_nth(inhSchema, exist_attno - 1);
+
+				/*
+				 * Check encryption parameter.  All parents must have the same
+				 * encryption settings for a column.
+				 */
+				if ((def->encryption && !attribute->attcek) ||
+					(!def->encryption && attribute->attcek))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && attribute->attcek)
+				{
+					/*
+					 * Merging the encryption properties of two encrypted
+					 * parent columns is not yet implemented.  Right now, this
+					 * would confuse the checks of the type etc. below (we
+					 * must check the physical and the real types against each
+					 * other, respectively), which might require a larger
+					 * restructuring.  For now, just give up here.
+					 */
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("multiple inheritance of encrypted columns is not implemented")));
+				}
+
+				/*
+				 * Must have the same type, typmod, and collation.
+				 */
 				typenameTypeIdAndMod(NULL, def->typeName, &defTypeId, &deftypmod);
 				if (defTypeId != attribute->atttypid ||
 					deftypmod != attribute->atttypmod)
@@ -2641,6 +2684,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+														attribute->attusertypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -2919,6 +2968,34 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								 errdetail("%s versus %s", def->compression, newdef->compression)));
 				}
 
+				/*
+				 * Check encryption parameter.  All parents and children must
+				 * have the same encryption settings for a column.
+				 */
+				if ((def->encryption && !newdef->encryption) ||
+					(!def->encryption && newdef->encryption))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && newdef->encryption)
+				{
+					FormData_pg_attribute a, newa;
+
+					GetColumnEncryption(def->encryption, &a);
+					GetColumnEncryption(newdef->encryption, &newa);
+
+					if (a.atttypid != newa.atttypid ||
+						a.atttypmod != newa.atttypmod ||
+						a.attcek != newa.attcek)
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" has an encryption specification conflict",
+										attributeName)));
+				}
+
 				/* Mark the column as locally defined */
 				def->is_local = true;
 				/* Merge of NOT NULL constraints = OR 'em together */
@@ -6861,6 +6938,19 @@ 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);
+		aclresult = object_aclcheck(ColumnEncKeyRelationId, attribute.attcek, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attribute.attcek, false));
+	}
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attusertypid = 0;
+		attribute.attusertypmod = -1;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12692,6 +12782,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:
@@ -19291,3 +19384,110 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	List	   *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 = defGetQualifiedName(el);
+		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);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				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 = get_cek_oid(cek, false);
+
+	attr->attcek = cekoid;
+	attr->attusertypid = attr->atttypid;
+	attr->attusertypmod = attr->atttypmod;
+
+	/* 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;
+
+	attr->atttypmod = alg;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	List	   *result;
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	char	   *nspname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attr->attcek));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attr->attcek);
+
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+	nspname = get_namespace_name(form->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(form->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->atttypmod))),
+									-1));
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index bb0f5de4c2..cec2daa9c3 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/commands/view.c b/src/backend/commands/view.c
index ff98c773f5..171dca3803 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index dc8415a693..c81bc15128 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3983,6 +3983,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/gram.y b/src/backend/parser/gram.y
index a0138382a1..a6039878fa 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,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
@@ -592,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
@@ -690,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
@@ -708,13 +711,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	JOIN
 
-	KEY
+	KEY KEYS
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
 	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
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -1988,7 +1993,7 @@ CheckPointStmt:
 
 /*****************************************************************************
  *
- * DISCARD { ALL | TEMP | PLANS | SEQUENCES }
+ * DISCARD
  *
  *****************************************************************************/
 
@@ -2014,6 +2019,13 @@ DiscardStmt:
 					n->target = DISCARD_TEMP;
 					$$ = (Node *) n;
 				}
+			| DISCARD COLUMN ENCRYPTION KEYS
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+
+					n->target = DISCARD_COLUMN_ENCRYPTION_KEYS;
+					$$ = (Node *) n;
+				}
 			| DISCARD PLANS
 				{
 					DiscardStmt *n = makeNode(DiscardStmt);
@@ -3700,14 +3712,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;
@@ -3716,8 +3729,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;
@@ -3774,6 +3787,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4052,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6289,33 @@ 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
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (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; }
@@ -6289,6 +6335,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)); }
@@ -6800,6 +6850,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -7611,6 +7663,24 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| COLUMN ENCRYPTION KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CEK;
+					n->objs = $4;
+					$$ = n;
+				}
+			| COLUMN MASTER KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CMK;
+					n->objs = $4;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9140,6 +9210,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9817,6 +9907,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10148,6 +10258,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11304,6 +11432,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_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 any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16965,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16840,6 +17015,7 @@ unreserved_keyword:
 			| INVOKER
 			| ISOLATION
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -16854,6 +17030,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17514,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17404,6 +17582,7 @@ bare_label_keyword:
 			| ISOLATION
 			| JOIN
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -17425,6 +17604,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 2240284f21..a8a1855aa0 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,159 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			Expr	   *expr = te->expr;
+
+			/*
+			 * If it's a RelabelType, look inside.  (For encrypted columns,
+			 * this would typically be a typmod adjustment.)
+			 */
+			if (IsA(expr, RelabelType))
+				expr = castNode(RelabelType, expr)->arg;
+
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(expr, Param))
+			{
+				Param	   *p = (Param *) expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(expr, Var))
+			{
+				Var		   *v = (Var *) expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell   *lc;
+
+					foreach(lc, srcrte->values_lists)
+					{
+						List	   *values_list = lfirst_node(List, lc);
+						Expr	   *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, RelabelType))
+							value = castNode(RelabelType, value)->arg;
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach(lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f9218f48aa..8d902292cd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1035,8 +1035,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+												attribute->attusertypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2552327d90..79ec9c4d1f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2210,12 +2210,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 cab709b07b..b577557384 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -71,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"
 
@@ -1815,6 +1817,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 $%d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2560,6 +2572,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2618,11 +2632,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	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 = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				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 + 1)));
+
+			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->attusertypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x0001;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&row_description_buf, (int) pcekid);
+			pq_sendint32(&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 c7d9d96b45..180f86cb79 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,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,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 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,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 +2665,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 +2791,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;
@@ -2917,6 +2954,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_ALL:
 					tag = CMDTAG_DISCARD_ALL;
 					break;
+				case DISCARD_COLUMN_ENCRYPTION_KEYS:
+					tag = CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS;
+					break;
 				case DISCARD_PLANS:
 					tag = CMDTAG_DISCARD_PLANS;
 					break;
@@ -3063,6 +3103,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3736,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 8f7522d103..6aeb06f8fd 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -22,6 +22,8 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_foreign_data_wrapper.h"
 #include "catalog/pg_foreign_server.h"
@@ -101,6 +103,10 @@ static AclMode convert_table_priv_string(text *priv_type_text);
 static AclMode convert_sequence_priv_string(text *priv_type_text);
 static AttrNumber convert_column_name(Oid tableoid, text *column);
 static AclMode convert_column_priv_string(text *priv_type_text);
+static Oid	convert_column_encryption_key_name(text *cekname);
+static AclMode convert_column_encryption_key_priv_string(text *priv_type_text);
+static Oid	convert_column_master_key_name(text *cmkname);
+static AclMode convert_column_master_key_priv_string(text *priv_type_text);
 static Oid	convert_database_name(text *databasename);
 static AclMode convert_database_priv_string(text *priv_type_text);
 static Oid	convert_foreign_data_wrapper_name(text *fdwname);
@@ -800,6 +806,14 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			/* for backwards compatibility, grant some rights by default */
 			world_default = ACL_CREATE_TEMP | ACL_CONNECT;
@@ -911,6 +925,12 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 's':
 			objtype = OBJECT_SEQUENCE;
 			break;
+		case 'Y':
+			objtype = OBJECT_CEK;
+			break;
+		case 'y':
+			objtype = OBJECT_CMK;
+			break;
 		case 'd':
 			objtype = OBJECT_DATABASE;
 			break;
@@ -2915,6 +2935,384 @@ convert_column_priv_string(text *priv_type_text)
 }
 
 
+/*
+ * has_column_encryption_key_privilege variants
+ *		These are all named "has_column_encryption_key_privilege" at the SQL level.
+ *		They take various combinations of column encryption key name,
+ *		cek OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_encryption_key_privilege_name_name
+ *		Check user privileges on a column encryption key given
+ *		name username, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name
+ *		Check user privileges on a column encryption key given
+ *		text cekname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cekname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name_id
+ *		Check user privileges on a column encryption key given
+ *		name usename, column encryption key oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id
+ *		Check user privileges on a column encryption key given
+ *		column encryption key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cekid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_name
+ *		Check user privileges on a column encryption key given
+ *		roleid, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_id
+ *		Check user privileges on a column encryption key given
+ *		roleid, cek oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_encryption_key_privilege family.
+ */
+
+/*
+ * Given a CEK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_encryption_key_name(text *cekname)
+{
+	return get_cek_oid(textToQualifiedNameList(cekname), false);
+}
+
+/*
+ * convert_column_encryption_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_encryption_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_encryption_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_encryption_key_priv_map);
+}
+
+
+/*
+ * has_column_master_key_privilege variants
+ *		These are all named "has_column_master_key_privilege" at the SQL level.
+ *		They take various combinations of column master key name,
+ *		cmk OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_master_key_privilege_name_name
+ *		Check user privileges on a column master key given
+ *		name username, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name
+ *		Check user privileges on a column master key given
+ *		text cmkname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cmkname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name_id
+ *		Check user privileges on a column master key given
+ *		name usename, column master key oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id
+ *		Check user privileges on a column master key given
+ *		column master key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cmkid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_name
+ *		Check user privileges on a column master key given
+ *		roleid, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_id
+ *		Check user privileges on a column master key given
+ *		roleid, cmk oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_master_key_privilege family.
+ */
+
+/*
+ * Given a CMK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_master_key_name(text *cmkname)
+{
+	return get_cmk_oid(textToQualifiedNameList(cmkname), false);
+}
+
+/*
+ * convert_column_master_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_master_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_master_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_master_key_priv_map);
+}
+
+
 /*
  * has_database_privilege variants
  *		These are all named "has_database_privilege" at the SQL level.
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 5778e3f0ef..dd21fcf59e 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -679,6 +679,113 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	Node	   *escontext = fcinfo->context;
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c07382051d..7ad159110f 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
  *
@@ -3683,3 +3705,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+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;
+}
+
+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 77c2ba3f8f..d40af13efe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 94abede512..eb432260da 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"
@@ -219,6 +222,32 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAMENSP] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameNspIndexId,
+		KEY(Anum_pg_colenckey_cekname,
+			Anum_pg_colenckey_ceknamespace),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +262,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAMENSP] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameNspIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname,
+			Anum_pg_colmasterkey_cmknamespace),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 033647011b..4b6be68f27 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"
@@ -130,6 +132,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())
 	{
 		/*
@@ -237,6 +245,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,
@@ -297,7 +311,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 a43f2e5553..e67462c81a 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -201,6 +203,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);
 
@@ -859,6 +867,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 9753a6d868..0a5178bcf1 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -484,6 +484,10 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('U', "USAGE");
 	}
+	else if (strcmp(type, "COLUMN ENCRYPTION KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "COLUMN MASTER KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "DATABASE") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,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 61ebb8fe85..bc303550fd 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3396,6 +3396,8 @@ _getObjectDescription(PQExpBuffer buf, const 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, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 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 24ba936332..8c61fcab13 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,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);
@@ -393,6 +396,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},
@@ -685,6 +689,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");
 
@@ -1056,6 +1063,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"
@@ -5571,6 +5579,164 @@ 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_ceknamespace;
+	int			i_cekowner;
+	int			i_cekacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, cek.cekowner, cek.cekacl, acldefault('Y', cek.cekowner) AS acldefault\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_ceknamespace = PQfnumber(res, "ceknamespace");
+	i_cekowner = PQfnumber(res, "cekowner");
+	i_cekacl = PQfnumber(res, "cekacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cekacl));
+		cekinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cekinfo[i].dacl.privtype = 0;
+		cekinfo[i].dacl.initprivs = NULL;
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, 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].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			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);
+		if (!PQgetisnull(res, i, i_cekacl))
+			cekinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	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_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+	int			i_cmkacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, cmk.cmkowner, cmk.cmkrealm, cmk.cmkacl, acldefault('y', cmk.cmkowner) AS acldefault\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_cmknamespace = PQfnumber(res, "cmknamespace");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+	i_cmkacl = PQfnumber(res, "cmkacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		cmkinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cmkacl));
+		cmkinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cmkinfo[i].dacl.privtype = 0;
+		cmkinfo[i].dacl.initprivs = NULL;
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cmkacl))
+			cmkinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8186,6 +8352,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8245,8 +8414,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
+	appendPQExpBuffer(q, "SELECT\n"
 						 "a.attrelid,\n"
 						 "a.attnum,\n"
 						 "a.attname,\n"
@@ -8259,7 +8427,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attlen,\n"
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
+						 "pg_catalog.format_type(%s) AS atttypname,\n"
 						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
@@ -8268,7 +8436,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "' ' || pg_catalog.quote_literal(option_value) "
 						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
 						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+						 "), E',\n    ') AS attfdwoptions,\n",
+						 fout->remoteVersion >= 160000 ?
+						 "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END, CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END" :
+						 "a.atttypid, a.atttypmod");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -8294,10 +8465,23 @@ 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,
+						  "a.attcek,\n"
+						  "CASE a.atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "CASE WHEN a.atttypid IN (%u, %u) THEN a.atttypmod END AS attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID,
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcek,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
@@ -8322,6 +8506,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	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");
@@ -8382,6 +8569,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->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		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));
@@ -8409,6 +8599,22 @@ 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_attcek))
+			{
+				Oid		attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[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));
@@ -9927,6 +10133,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;
@@ -13320,6 +13532,141 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * 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",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_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,
+								  .namespace = cekinfo->dobj.namespace->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,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cekinfo->dobj.dumpId, InvalidDumpId, "COLUMN ENCRYPTION KEY",
+				qcekname, NULL, cekinfo->dobj.namespace->dobj.name,
+				cekinfo->rolname, &cekinfo->dacl);
+
+	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",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	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,
+								  .namespace = cmkinfo->dobj.namespace->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,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cmkinfo->dobj.dumpId, InvalidDumpId, "COLUMN MASTER KEY",
+				qcmkname, NULL, cmkinfo->dobj.namespace->dobj.name,
+				cmkinfo->rolname, &cmkinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15403,6 +15750,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[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_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -17971,6 +18334,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 cdca0b993d..d4a2e595d0 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,
@@ -332,6 +334,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	struct _CekInfo **attcek;
+	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 */
@@ -663,6 +668,32 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	struct _CmkInfo **cekcmks;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -683,6 +714,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -710,6 +743,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 8266c117a3..d3dacd39da 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 cd421c5944..6530fb81a2 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 72b19ee6cd..837d515639 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -719,6 +719,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, 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, },
@@ -1319,6 +1331,26 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1737,6 +1769,26 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3570,6 +3622,26 @@
 		unlike => { no_privs => 1, },
 	},
 
+	'GRANT USAGE ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
+	'GRANT USAGE ON COLUMN MASTER KEY cmk1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
 	'GRANT USAGE ON FOREIGN DATA WRAPPER dummy' => {
 		create_order => 85,
 		create_sql   => 'GRANT USAGE ON FOREIGN DATA WRAPPER dummy
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..0d6a46d24b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index c8a0bb7b3a..04d437f836 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1530,7 +1530,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;
@@ -1546,6 +1546,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;
@@ -1568,6 +1569,8 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attusertypid;
+	const char *attusertypmod;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1844,7 +1847,17 @@ 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)");
+	if (pset.sversion >= 160000)
+	{
+		attusertypid = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END";
+		attusertypmod = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END";
+	}
+	else
+	{
+		attusertypid = "a.atttypid";
+		attusertypmod = "a.atttypmod";
+	}
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, %s)", attusertypid, attusertypmod);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1857,7 +1870,8 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		attrdef_col = cols++;
 		attnotnull_col = cols++;
-		appendPQExpBufferStr(&buf, ",\n  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
+		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");
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
@@ -1909,6 +1923,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			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 ||
@@ -2032,6 +2058,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)
@@ -2124,6 +2152,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),
@@ -4477,6 +4516,152 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cekacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cmkacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cmk.oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 554fe86725..1cf8f72176 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1729966959 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,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 73d4b393bc..010bc5a6d5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,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 5a28b6f713..6736505c3a 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 5e1882eaea..0642449fa0 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -933,6 +933,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1223,6 +1237,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
@@ -1705,7 +1721,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1952,6 +1968,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_SCHEMA_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", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2894,6 +2926,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",
@@ -3605,7 +3657,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 	else if (Matches("DISCARD"))
-		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP");
+		COMPLETE_WITH("ALL", "COLUMN ENCRYPTION KEYS", "PLANS", "SEQUENCES", "TEMP");
 
 /* DO */
 	else if (Matches("DO"))
@@ -3619,6 +3671,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) ||
@@ -3931,6 +3984,8 @@ psql_completion(const char *text, int start, int end)
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"COLUMN ENCRYPTION KEY",
+											"COLUMN MASTER KEY",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4046,6 +4101,16 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+	/* Complete "GRANT/REVOKE * ON COLUMN ENCRYPTION|MASTER KEY *" with TO/FROM */
+	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
+			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny))
+	{
+		if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny, MatchAny))
+			COMPLETE_WITH("TO");
+		else
+			COMPLETE_WITH("FROM");
+	}
+
 	/* Complete "GRANT/REVOKE * ON FOREIGN DATA WRAPPER *" with TO/FROM */
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny))
diff --git a/src/common/Makefile b/src/common/Makefile
index 113029bf7b..73dce1150e 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		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";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index 41bd58ebdf..3695d3285b 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -4,6 +4,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 747ecb800d..4e384bbcdb 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,9 +20,13 @@ 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);
 
+extern void DiscardColumnEncryptionKeys(void);
+
 extern void debugStartup(DestReceiver *self, int operation,
 						 TupleDesc typeinfo);
 extern bool debugtup(TupleTableSlot *slot, DestReceiver *self);
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index ffd5e9dc82..eb59e73c0a 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/heap.h b/src/include/catalog/heap.h
index d01ab504b6..758696b539 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 3179be09d3..9e2c5256f7 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,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/namespace.h b/src/include/catalog/namespace.h
index f64a0ec26b..d0b9e8458d 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -115,6 +115,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c4d6adcd3e..c58b79e3a7 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 5b950129de..0e9e85ebf3 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 b561e17781..7910175a6a 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,17 @@ 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);
+
+	/*
+	 * User-visible type and typmod, currently used for encrypted columns.
+	 * These are only set to nondefault values if they are different from
+	 * atttypid and attypmod.
+	 */
+	Oid			attusertypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+	int32		attusertypmod BKI_DEFAULT(-1);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..c57fa18a27
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		cekacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_TOAST(pg_colenckey, 8263, 8264);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_nsp_index, 8242, ColumnEncKeyNameNspIndexId, on pg_colenckey using btree(cekname name_ops, ceknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /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-2023, 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);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..d3bfd36279
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+	aclitem		cmkacl[1] BKI_DEFAULT(_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_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, on pg_colmasterkey using btree(cmkname name_ops, cmknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c867d99563..ff06d52fd0 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 b2cdea66c4..114279fa64 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 91587b99d0..c21052a3f7 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 505595620e..b410388a42 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6355,6 +6355,14 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -7142,6 +7150,68 @@
   proname => 'fmgr_sql_validator', provolatile => 's', prorettype => 'void',
   proargtypes => 'oid', prosrc => 'fmgr_sql_validator' },
 
+{ oid => '8265',
+  descr => 'user privilege on column encryption key by username, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_encryption_key_privilege_name_name' },
+{ oid => '8266',
+  descr => 'user privilege on column encryption key by username, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_encryption_key_privilege_name_id' },
+{ oid => '8267',
+  descr => 'user privilege on column encryption key by user oid, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_encryption_key_privilege_id_name' },
+{ oid => '8268',
+  descr => 'user privilege on column encryption key by user oid, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_encryption_key_privilege_id_id' },
+{ oid => '8269',
+  descr => 'current user privilege on column encryption key by column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_encryption_key_privilege_name' },
+{ oid => '8270',
+  descr => 'current user privilege on column encryption key by column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_encryption_key_privilege_id' },
+
+{ oid => '8271',
+  descr => 'user privilege on column master key by username, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_master_key_privilege_name_name' },
+{ oid => '8272',
+  descr => 'user privilege on column master key by username, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_master_key_privilege_name_id' },
+{ oid => '8273',
+  descr => 'user privilege on column master key by user oid, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_master_key_privilege_id_name' },
+{ oid => '8274',
+  descr => 'user privilege on column master key by user oid, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_master_key_privilege_id_id' },
+{ oid => '8275',
+  descr => 'current user privilege on column master key by column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_master_key_privilege_name' },
+{ oid => '8276',
+  descr => 'current user privilege on column master key by column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_master_key_privilege_id' },
+
 { oid => '2250',
   descr => 'user privilege on database by username, database name',
   proname => 'has_database_privilege', provolatile => 's', prorettype => 'bool',
@@ -11939,4 +12009,37 @@
   proname => 'any_value_transfn', prorettype => 'anyelement',
   proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_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 92bcaf2c73..b9d3920a97 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,17 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+# Note: typstorage 'e' since compression is not useful for encrypted data
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 519e570c8c..3c7ab2a8fe 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 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, 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);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e7c2b91a58..11f88c59ce 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,4 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml>.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index ac6407e9f6..39a286e58a 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 f7d7f10f7d..1699e2a63f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -88,8 +88,7 @@ typedef uint64 AclMode;			/* a bitmask of privilege bits */
 #define ACL_REFERENCES	(1<<5)
 #define ACL_TRIGGER		(1<<6)
 #define ACL_EXECUTE		(1<<7)	/* for functions */
-#define ACL_USAGE		(1<<8)	/* for languages, namespaces, FDWs, and
-								 * servers */
+#define ACL_USAGE		(1<<8)	/* for various object types */
 #define ACL_CREATE		(1<<9)	/* for namespaces and databases */
 #define ACL_CREATE_TEMP (1<<10) /* for databases */
 #define ACL_CONNECT		(1<<11) /* for databases */
@@ -722,6 +721,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? */
@@ -758,11 +758,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1980,6 +1981,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2167,6 +2171,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
@@ -3641,6 +3670,7 @@ typedef struct CheckPointStmt
 typedef enum DiscardMode
 {
 	DISCARD_ALL,
+	DISCARD_COLUMN_ENCRYPTION_KEYS,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
 	DISCARD_TEMP
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..c6da932042 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)
@@ -228,6 +229,7 @@ PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL)
 PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("language", LANGUAGE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("large", LARGE_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -250,6 +252,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_param.h b/src/include/parser/parse_param.h
index d4865e50f6..26e1cd617a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..a5e61c9f5b 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)
@@ -130,6 +134,7 @@ PG_CMDTAG(CMDTAG_DECLARE_CURSOR, "DECLARE CURSOR", false, false, false)
 PG_CMDTAG(CMDTAG_DELETE, "DELETE", false, false, true)
 PG_CMDTAG(CMDTAG_DISCARD, "DISCARD", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
+PG_CMDTAG(CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS, "DISCARD COLUMN ENCRYPTION KEYS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false)
@@ -138,6 +143,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/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2..0c73022833 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -159,6 +159,8 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
+#define ACL_ALL_RIGHTS_CEK			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_CMK			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..1dac24e4b4 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,9 @@ 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 char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, 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 a443181d41..8ecacb29df 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d5d50ceab4..bf1d527c1a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAMENSP,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAMENSP,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c18e914228..10dfda8016 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 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
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 8f80c35c89..df701f2df3 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}
@@ -1412,6 +1420,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4056,6 +4086,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 0000000000..9baa47da13
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,839 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, 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 "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+#define pqResultAlloc(res, nBytes, isBinary) malloc(nBytes)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = 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;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								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);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05>
+ * for encrypting and decrypting column values.
+ */
+
+#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,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+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;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+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;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+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,
+					 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;
+	int64		al;
+	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;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	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;
+}
+
+/*
+ * Decrypt a column value
+ */
+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			iv_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 = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_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;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_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;
+	buf = pqResultAlloc(res, bufsize, false);
+	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;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#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)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "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 */
+
+/*
+ * Encrypt a column value
+ */
+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			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_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)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	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, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								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,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, 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, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, 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, const char *cmkfilename, 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 ec62550e38..128f8e3b96 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,420 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+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 *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					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, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	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;
+
+	if (!conn->cmklookup || !conn->cmklookup[0])
+	{
+		libpq_append_conn_error(conn, "column master key lookup is not configured");
+		return NULL;
+	}
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', 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)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, "r");
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+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
@@ -1251,13 +1668,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				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("protocol error: column encryption key associated with encrypted column was not sent by the server");
+					goto fail;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				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;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* 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;
@@ -1500,6 +1955,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1973,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2012,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2128,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2171,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2270,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2319,47 @@ PQsendQueryGuts(PGconn *conn,
 		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;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2378,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2396,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#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)
+				{
+					libpq_append_conn_error(conn, "protocol error: column encryption key associated with encrypted parameter was not sent by the server");
+					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
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2878,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4146,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4194,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)
 {
@@ -3762,6 +4390,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 8ab6a88416..617e641cf6 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);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -358,8 +366,21 @@ pqParseInput3(PGconn *conn)
 					}
 					break;
 				case 't':		/* Parameter Description */
-					if (getParamDescriptions(conn, msgLength))
-						return;
+					if (conn->error_result ||
+						(conn->result != NULL &&
+						 conn->result->resultStatus == PGRES_FATAL_ERROR))
+					{
+						/*
+						 * We've already choked for some reason.  Just discard
+						 * the data till we get to the end of the query.
+						 */
+						conn->inCursor += msgLength;
+					}
+					else
+					{
+						if (getParamDescriptions(conn, msgLength))
+							return;
+					}
 					break;
 				case 'D':		/* Data Row */
 					if (conn->result != NULL &&
@@ -547,6 +568,9 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		int			typlen;
 		int			atttypmod;
 		int			format;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGets(&conn->workBuffer, conn) ||
 			pqGetInt(&tableid, 4, conn) ||
@@ -561,6 +585,23 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, conn) ||
+				pqGetInt(&flags, 2, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -582,6 +623,8 @@ 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)
 			result->binary = 0;
@@ -685,10 +728,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, 4, 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! */
@@ -1468,6 +1532,92 @@ 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);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	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;
+	int			ret;
+
+	/* 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, 4, 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 */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2436,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 abaab6a073..77791e8349 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);
+			pqTraceOutputInt32(f, message, cursor, false);
+			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);
+			pqTraceOutputInt32(f, message, cursor, false);
+		}
 	}
 }
 
@@ -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);
+	pqTraceOutputInt32(f, message, cursor, false);
+	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 f3d9220496..cf339a3e53 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,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,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 +559,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 d7ec5ed429..cbfaa7f95f 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);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 573fd9b6ea..a6e6b2ada9 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,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
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': get_option('ssl')},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 4df544ecef..0c36aa5f32 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -1,6 +1,6 @@
 # src/interfaces/libpq/nls.mk
 CATALOG_NAME     = libpq
-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_append_conn_error:2 \
                    libpq_append_error:2 \
                    libpq_gettext pqInternalNotice:2
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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 b2a4b06fd2..87d2808b52 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c8ba170503 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 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..d5ead874e5
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, 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
+
+export OPENSSL PERL
+
+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 0000000000..47f88c41ce
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,24 @@
+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',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+      'PERL': perl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..c56af737ac
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,255 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+my $perl = $ENV{PERL};
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$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"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	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}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# 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\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[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 %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = "*=run:$perl ./test_run_decrypt.pl %k %a %p";
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$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\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$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 update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$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', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+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');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$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 tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$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 second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..14eafb8ec9
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, 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 $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# 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, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+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, algorithm = '$cmkalg', 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 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	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;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	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 = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	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
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/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-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $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}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..d55ddb1ab7 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,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 0000000000..df47e36280
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,451 @@
+\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;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+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       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  permission denied for column master key cmk1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+ERROR:  permission denied for column encryption key cek1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column 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 in schema "public"
+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 in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION '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 SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf..77fedce8aa 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -107,7 +109,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
@@ -327,6 +330,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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+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},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 -- these object types cannot be qualified names
 SELECT pg_get_object_address('language', '{one}', '{}');
 ERROR:  language "one" does not exist
@@ -409,6 +430,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}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,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
@@ -547,6 +576,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
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|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 215eb899be..226f5e404e 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 {attusertypid} => 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,9 @@ 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 {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {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 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ 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)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,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
@@ -872,6 +876,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
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 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,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 15e015b3d6..577a6d4b2e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 427429975e..57c7d2bec3 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..8d6320015c
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,297 @@
+\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;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+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
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+
+
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+
+
+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 SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..35af43032f 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -99,7 +101,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
@@ -174,6 +177,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}', '{}'),
@@ -228,6 +234,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;
 
@@ -247,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
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 71a75626d5271f2bcdbdc43b8c13065c4634fd9f
-- 
2.39.2

#71Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#70)
1 attachment(s)
Re: Transparent column encryption

Here is an updated patch. I have done some cosmetic polishing and fixed
a minor Windows-related bug.

In my mind, the patch is complete.

If someone wants to do some in-depth code review, I suggest focusing on
the following files:

* src/backend/access/common/printtup.c
* src/backend/commands/colenccmds.c
* src/backend/commands/tablecmds.c
* src/backend/parser/parse_param.c
* src/interfaces/libpq/fe-exec.c
* src/interfaces/libpq/fe-protocol3.c
* src/interfaces/libpq/libpq-fe.h

(Most other files are DDL boilerplate or otherwise not too interesting.)

Attachments:

v18-0001-Automatic-client-side-column-level-encryption.patchtext/plain; charset=UTF-8; name=v18-0001-Automatic-client-side-column-level-encryption.patchDownload
From c73c3f5988aa8c96b7f480916c0eac50ea8b6c1c Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Fri, 10 Mar 2023 08:07:00 +0100
Subject: [PATCH v18] Automatic client-side column-level encryption

This feature enables the automatic 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 asymmetric 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
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.

To get automatically encrypted data into the database (as opposed to
reading it out), it is required to use protocol-level prepared
statements (i.e., extended query).  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.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this if the
\bind command is used.

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.

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                    | 317 +++++++
 doc/src/sgml/charset.sgml                     |  10 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 444 +++++++++
 doc/src/sgml/func.sgml                        |  60 ++
 doc/src/sgml/glossary.sgml                    |  26 +
 doc/src/sgml/libpq.sgml                       | 322 +++++++
 doc/src/sgml/protocol.sgml                    | 467 ++++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |  10 +
 .../ref/create_column_encryption_key.sgml     | 173 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  55 +-
 doc/src/sgml/ref/discard.sgml                 |  14 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/grant.sgml                   |  12 +-
 doc/src/sgml/ref/pg_dump.sgml                 |  42 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   8 +
 src/backend/access/common/printtup.c          | 237 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  60 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 288 ++++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  17 +
 src/backend/commands/colenccmds.c             | 439 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/discard.c                |   8 +-
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 197 ++++
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 200 ++++-
 src/backend/parser/parse_param.c              | 157 ++++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  56 ++
 src/backend/utils/adt/acl.c                   | 398 +++++++++
 src/backend/utils/adt/varlena.c               | 107 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  42 +
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/dumputils.c                   |   4 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 377 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  35 +
 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              |  72 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 191 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  69 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 104 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   4 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |  11 +
 src/include/catalog/pg_colenckey.h            |  46 +
 src/include/catalog/pg_colenckeydata.h        |  46 +
 src/include/catalog/pg_colmasterkey.h         |  47 +
 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               | 103 +++
 src/include/catalog/pg_type.dat               |  13 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  41 +-
 src/include/parser/kwlist.h                   |   3 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   7 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 839 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 671 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 157 +++-
 src/interfaces/libpq/fe-trace.c               |  56 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   1 +
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  23 +
 .../t/001_column_encryption.pl                | 257 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 451 ++++++++++
 src/test/regress/expected/object_address.out  |  35 +
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 297 +++++++
 src/test/regress/sql/object_address.sql       |  11 +
 src/test/regress/sql/type_sanity.sql          |   2 +
 144 files changed, 10393 insertions(+), 84 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/common/colenc.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/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 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 2df6559acc..3a5f2c254c 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; see <xref linkend="ddl-column-encryption"/>
+     </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; see <xref linkend="ddl-column-encryption"/>
+     </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 746baf5053..54c5199e0f 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,44 @@ <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>attusertypid</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.  If the column is not
+       encrypted, then 0.  For encrypted columns, the field
+       <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.
+       </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type
+       modifier (analogous to <structfield>atttypmod</structfield>) that is
+       reported to the client.  If the column is not encrypted, then -1.  For
+       encrypted columns, the field <structfield>atttypmod</structfield>)
+       contains 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>
@@ -2476,6 +2529,270 @@ <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>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </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>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </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>int4</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>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </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>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml
index 3032392b80..f3026fff83 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1721,6 +1721,16 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When automatic client-side column-level encryption is used, then no
+     encoding conversion is possible.  (The encoding conversion happens on the
+     server, and the server cannot look inside any encrypted column values.)
+     If automatic client-side column-level encryption is enabled for a
+     session, then the server enforces that the client encoding matches the
+     server encoding, and any attempts to change the client encoding will be
+     rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..67fce16872 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <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
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support automatic client-side column-level encryption
+    or have disabled it will see the encrypted values in this format.  Clients
+    that support automatic client-side column-level encryption will not see
+    these types in result sets, as the protocol layer will translate them back
+    to the 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 5179125510..65514e119f 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,440 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   With <firstterm>automatic client-side column-level encryption</firstterm>,
+   columns can be stored encrypted in the database.  The encryption and
+   decryption happens automatically 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 Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   Automatic client-side column-level 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 asymmetric 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 from making
+   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 automatic client-side column-level
+   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 automatic client-side column-level
+       encryption.  Not all client libraries do.  Furthermore, the client
+       library might require that automatic client-side column-level
+       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>
+    would 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 employees (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This would leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of client-side column-level encryption.
+    (And even ignoring that, it could not work because the server does not
+    have access to the keys to perform the 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>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</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>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   The steps to set up automatic client-side column-level 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"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</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, algorithm = 'RSAES_OAEP_SHA_1', 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 automatic client-side column-level 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 Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use automatic client-side column-level 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 transport 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 allows 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>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation; for
+    <application>libpq</application>, see <xref
+    linkend="libpq-connect-column-encryption"/>).
+   </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, 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.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <tip>
+    <para>
+     One might be inclined to think of the client-side column-level encryption
+     feature as a mechanism for application writers and users to protect
+     themselves against an <quote>evil DBA</quote>, but that is not the
+     intended purpose.  Rather, it is (also) a tool for the DBA to control
+     which data they do not want (in plaintext) on the server.
+    </para>
+   </tip>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on 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>
 
@@ -1986,6 +2420,14 @@ <title>Privileges</title>
        server.  Grantees may also create, alter, or drop their own user
        mappings associated with that server.
       </para>
+      <para>
+       For column master keys, allows the creation of column encryption keys
+       using the master key.
+      </para>
+      <para>
+       For column encryption keys, allows the use of the key in the creation
+       of table columns.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2152,6 +2594,8 @@ <title>ACL Privilege Abbreviations</title>
       <entry><literal>USAGE</literal></entry>
       <entry><literal>U</literal></entry>
       <entry>
+       <literal>COLUMN ENCRYPTION KEY</literal>,
+       <literal>COLUMN MASTER KEY</literal>,
        <literal>DOMAIN</literal>,
        <literal>FOREIGN DATA WRAPPER</literal>,
        <literal>FOREIGN SERVER</literal>,
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 9c6107f960..88e2b30f13 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22859,6 +22859,40 @@ <title>Access Privilege Inquiry Functions</title>
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_encryption_key_privilege</primary>
+        </indexterm>
+        <function>has_column_encryption_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cek</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column encryption key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_master_key_privilege</primary>
+        </indexterm>
+        <function>has_column_master_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cmk</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column master key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
@@ -23349,6 +23383,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 7c01a541fe..818038d860 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,32 @@ <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 automatic
+     client-side column-level encryption (<xref
+     linkend="ddl-column-encryption"/>).  Column encryption keys are stored in
+     the database encrypted by another key, the <glossterm
+     linkend="glossary-column-master-key">column master key</glossterm>.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt <glossterm
+     linkend="glossary-column-encryption-key">column encryption
+     keys</glossterm>.  (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 3ccd8ff942..26cab10104 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,141 @@ <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 <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, this enables automatic client-side column-level
+        encryption for the connection.  If encrypted columns are queried and
+        this is not enabled, the encrypted values are 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>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </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>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </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 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 --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2999,32 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            half-byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this half-byte to one to force
+            encryption.  For example, use the C code literal
+            <literal>0x10</literal> to specify text format with forced
+            encryption.  If the array pointer is null then encryption is not
+            forced for any parameter.
+           </para>
+
+           <para>
+            Parameters corresponding to encrypted columns must be passed in
+            text format.  Specifying binary format for such a parameter will
+            result in an error.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter 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 compromised 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.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3037,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3196,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</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 *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> 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>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4084,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>
 
@@ -4059,6 +4287,31 @@ <title>Retrieving Query Result Information</title>
      </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-PQexecPreparedDescribed"/> 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>
 
@@ -4584,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4845,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4902,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4963,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</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 PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> 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-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5052,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8086,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 73b7f4432f..c43c3051c3 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,76 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-column-encryption">
+   <title>Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    Automatic client-side column-level 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 automatic client-side column-level 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 automatic column-level 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, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column-level encryption protocol option is enabled.  That way, a client
+    library only needs to support decrypting data sent in binary and does not
+    have to support decoding the text format of the encryption-related types
+    (see <xref linkend="datatype-encrypted"/>).
+   </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 automatic client-side column-level 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 automatic client-side column-level
+    encryption are described in <xref
+    linkend="protocol-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -3841,6 +3911,16 @@ <title>Message Formats</title>
          The parameter format codes.  Each must presently be
          zero (text) or one (binary).
         </para>
+
+        <para>
+         If the protocol extension <literal>_pq_.column_encryption</literal>
+         is enabled (see <xref linkend="protocol-flow-column-encryption"/>),
+         then the second-least-significant half-byte is set to one if the
+         parameter was encrypted by the client.  (So, for example, to send an
+         encrypted value in binary, the field is set to 0x11 in total.)  This
+         is used by the server to check that a parameter that was required to
+         be encrypted was actually encrypted.
+        </para>
        </listitem>
       </varlistentry>
 
@@ -4061,6 +4141,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-flow-column-encryption"/>.)
+     </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>Int32</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-flow-column-encryption"/>.)
+     </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>
@@ -5164,6 +5378,45 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), 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>Int32</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 parameter is to be
+         encrypted and bit 0x0001 is set, the column underlying the parameter
+         uses deterministic encryption, otherwise randomized encryption.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5552,6 +5805,50 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), 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>Int32</term>
+       <listitem>
+        <para>
+         If the field is 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 field is encrypted and
+         bit 0x0001 is set, the field uses deterministic encryption, otherwise
+         randomized encryption.
+        </para>
+        <!--
+            This is not really useful here, but it keeps alignment with
+            ParameterDescription.  Future flags might be useful in both
+            places.
+        -->
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7377,6 +7674,176 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-column-encryption-crypto">
+  <title>Automatic Client-side Column-level Encryption Cryptography</title>
+
+  <para>
+   This section describes the cryptographic operations used by the automatic
+   client-side column-level encryption functionality.  A client that supports
+   this functionality 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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</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>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</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>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</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>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</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>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</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 version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </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 IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </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 54b5f22d6e..a730e5d650 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 0000000000..655e1e00d8
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+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 }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</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
+   schema.  (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 (optionally schema-qualified) 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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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 0000000000..7f0e656ef0
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+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> ( REALM = <replaceable>realm</replaceable> )
+
+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 }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </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 schema.
+   (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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..1caf9bfa56 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..ebcbf5d00a 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,16 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support automatic client-side
+    column-level encryption or decryption; its input or output data will
+    always be the ciphertext.  This is usually suitable for backups (see also
+    <xref linkend="app-pgdump"/>).  If automatic client-side encryption or
+    decryption is wanted, <command>INSERT</command> and
+    <command>SELECT</command> need to be used instead to write and read the
+    data.
+   </para>
+
  </refsect1>
 
  <refsect1>
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 0000000000..65534fb03f
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,173 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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.  You must have <literal>USAGE</literal> privilege on the
+      column master 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>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></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.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </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,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    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,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    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 0000000000..6aaa1088d1
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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 a03dee4afe..d1549c7f45 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 | DEFAULT } ] [ 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 | DEFAULT } ] [ 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> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,47 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables automatic client-side column-level 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.  You must have <literal>USAGE</literal> privilege
+          on the column encryption key.
+         </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 id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -704,6 +745,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</literal></term>
         <listitem>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523ca..6a94706ef7 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@
 
  <refsynopsisdiv>
 <synopsis>
-DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
+DISCARD { ALL | COLUMN ENCRYPTION KEYS | PLANS | SEQUENCES | TEMPORARY | TEMP }
 </synopsis>
  </refsynopsisdiv>
 
@@ -42,6 +42,17 @@ <title>Parameters</title>
 
   <variablelist>
 
+   <varlistentry>
+    <term><literal>COLUMN ENCRYPTION KEYS</literal></term>
+    <listitem>
+     <para>
+      Discards knowledge about which column encryption keys and column master
+      keys have been sent to the client in this session.  (They will
+      subsequently be re-sent as required.)
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>PLANS</literal></term>
     <listitem>
@@ -93,6 +104,7 @@ <title>Parameters</title>
 DISCARD PLANS;
 DISCARD TEMP;
 DISCARD SEQUENCES;
+DISCARD COLUMN ENCRYPTION KEYS;
 </programlisting></para>
     </listitem>
    </varlistentry>
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 0000000000..f2ac1beb08
--- /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 (optionally schema-qualified) 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 0000000000..fae95e09d1
--- /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 (optionally schema-qualified) 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/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 35bf0332c8..f712f8e9e4 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -46,6 +46,16 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN ENCRYPTION KEY <replaceable>cek_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN MASTER KEY <replaceable>cmk_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON DOMAIN <replaceable>domain_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -513,7 +523,7 @@ <title>Compatibility</title>
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
+    Privileges on databases, tablespaces, schemas, keys, languages, and
     configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 334e4b7fd1..5412ea8666 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -716,6 +716,48 @@ <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 turns on the column encryption connection option in
+        <application>libpq</application> (see <xref
+        linkend="libpq-connect-column-encryption"/>).  Column master key
+        lookup must be configured by the user, either through a connection
+        option or an environment setting (see <xref
+        linkend="libpq-connect-cmklookup"/>).
+       </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.  (But then it is recommended
+        to not do this on the same host as the server, to avoid exposing
+        unencrypted data that is meant to be kept encrypted on the server.)
+        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 ... \bind ... \g 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 e62d05e5ab..4bf60c729f 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 7b8ae9fac3..b6115e433e 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4029,6 +4057,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <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 id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..c898997915 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 ef818228ac..c5894893eb 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,12 @@ 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_sendint32(&buf, 0);	/* CEK alg */
+			pq_sendint16(&buf, 0);	/* flags */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 72faeb5dfa..2d627b6f47 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,166 @@ 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;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	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_sendint32(&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;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	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);
+}
+
+void
+DiscardColumnEncryptionKeys(void)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -167,6 +342,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 +359,18 @@ 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(int32)		/* attencalg */
+			   + sizeof(int16));	/* flags */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +380,9 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
+		int16		flags = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +414,31 @@ 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->attusertypid;
+			atttypmod = orig_att->attusertypmod;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->atttypmod;
+			if (orig_att->atttypid == PG_ENCRYPTED_DETOID)
+				flags |= 0x0001;
+			ReleaseSysCache(tp);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +446,12 @@ 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_writeint32(buf, attencalg);
+			pq_writeint16(buf, flags);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +485,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 72a2c3d3db..f86ba299c3 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->attusertypid != attr2->attusertypid)
+			return false;
+		if (attr1->attusertypmod != attr2->attusertypmod)
+			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->attusertypid = 0;
+	att->attusertypmod = -1;
 	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->attusertypid = 0;
+	att->attusertypmod = -1;
 	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 24bab58499..cc48069932 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 a60107bf94..7b9575635b 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 c4232344aa..a3547c6cae 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.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"
@@ -247,6 +249,12 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_SEQUENCE:
 			whole_mask = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			whole_mask = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			whole_mask = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			whole_mask = ACL_ALL_RIGHTS_DATABASE;
 			break;
@@ -473,6 +481,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
 			errormsg = gettext_noop("invalid privilege type %s for sequence");
 			break;
+		case OBJECT_CEK:
+			all_privileges = ACL_ALL_RIGHTS_CEK;
+			errormsg = gettext_noop("invalid privilege type %s for column encryption key");
+			break;
+		case OBJECT_CMK:
+			all_privileges = ACL_ALL_RIGHTS_CMK;
+			errormsg = gettext_noop("invalid privilege type %s for column master key");
+			break;
 		case OBJECT_DATABASE:
 			all_privileges = ACL_ALL_RIGHTS_DATABASE;
 			errormsg = gettext_noop("invalid privilege type %s for database");
@@ -597,6 +613,12 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_SEQUENCE:
 			ExecGrant_Relation(istmt);
 			break;
+		case OBJECT_CEK:
+			ExecGrant_common(istmt, ColumnEncKeyRelationId, ACL_ALL_RIGHTS_CEK, NULL);
+			break;
+		case OBJECT_CMK:
+			ExecGrant_common(istmt, ColumnMasterKeyRelationId, ACL_ALL_RIGHTS_CMK, NULL);
+			break;
 		case OBJECT_DATABASE:
 			ExecGrant_common(istmt, DatabaseRelationId, ACL_ALL_RIGHTS_DATABASE, NULL);
 			break;
@@ -676,6 +698,26 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 				objects = lappend_oid(objects, relOid);
 			}
 			break;
+		case OBJECT_CEK:
+			foreach(cell, objnames)
+			{
+				List	   *cekname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cek_oid(cekname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
+		case OBJECT_CMK:
+			foreach(cell, objnames)
+			{
+				List	   *cmkname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cmk_oid(cmkname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
 		case OBJECT_DATABASE:
 			foreach(cell, objnames)
 			{
@@ -2693,6 +2735,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("permission denied for column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("permission denied for column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -2798,6 +2846,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:
@@ -2828,6 +2877,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;
@@ -2938,6 +2993,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:
@@ -3019,6 +3075,10 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
+		case OBJECT_CEK:
+			return object_aclmask(ColumnEncKeyRelationId, object_oid, roleid, mask, how);
+		case OBJECT_CMK:
+			return object_aclmask(ColumnMasterKeyRelationId, object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
 		case OBJECT_FUNCTION:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..cab6cfd140 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 */
@@ -1493,6 +1499,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 4f006820b8..df282c796f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,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_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,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_attusertypid - 1] = ObjectIdGetDatum(attrs->attusertypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypmod - 1] = Int32GetDatum(attrs->attusertypmod);
 		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)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attusertypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attusertypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 14e57adee2..00f914bc5f 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -26,6 +26,8 @@
 #include "catalog/dependency.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1997,6 +1999,254 @@ OpfamilyIsVisible(Oid opfid)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified parser name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4567,6 +4817,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(OpfamilyIsVisible(oid));
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2f688166e1..6c4ab9ac59 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,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		Anum_pg_colenckey_cekowner,
+		Anum_pg_colenckey_cekacl,
+		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,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		Anum_pg_colmasterkey_cmkowner,
+		Anum_pg_colmasterkey_cmkacl,
+		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
@@ -1029,6 +1086,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1108,6 +1175,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, 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));
@@ -2311,6 +2393,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2367,6 +2451,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2489,6 +2574,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2575,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3040,6 +3128,92 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				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);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				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 data of %s for %s"),
+								 getObjectDescription(&(ObjectAddress){ColumnEncKeyRelationId, cekdata->ckdcekid}, false),
+								 getObjectDescription(&(ObjectAddress){ColumnMasterKeyRelationId, cekdata->ckdcmkid}, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  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_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4440,6 +4614,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);
@@ -4905,6 +5091,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, 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",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, 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 48f7348f91..69f6175c60 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 bea51b3af1..59c23e9ef8 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"
@@ -118,6 +120,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -379,6 +387,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:
@@ -527,6 +537,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -643,6 +655,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:
@@ -876,6 +891,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 0000000000..3ada6d5aeb
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,439 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, 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/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.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, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	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)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	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")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	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, int 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] = Int32GetDatum(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)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+	nulls[Anum_pg_colenckey_cekacl - 1] = true;
+
+	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 (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(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);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+		AclResult	aclresult;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		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\"",
+						   NameListToString(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)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	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);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+	nulls[Anum_pg_colmasterkey_cmkacl - 1] = true;
+
+	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;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	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)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d6c6d514f3..9685b7886a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 296dc82d2e..86d22ca065 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/printtup.h"
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "commands/async.h"
@@ -25,7 +26,7 @@
 static void DiscardAll(bool isTopLevel);
 
 /*
- * DISCARD { ALL | SEQUENCES | TEMP | PLANS }
+ * DISCARD
  */
 void
 DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
@@ -36,6 +37,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			DiscardAll(isTopLevel);
 			break;
 
+		case DISCARD_COLUMN_ENCRYPTION_KEYS:
+			DiscardColumnEncryptionKeys();
+			break;
+
 		case DISCARD_PLANS:
 			ResetPlanCache();
 			break;
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	DiscardColumnEncryptionKeys();
 }
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 82bda15889..b4c681005a 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +517,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..4c1628cf7b 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 42cced9ebe..4b5ac30441 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..93d61c96ad 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 3e2c5f797c..11fa8e741c 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"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,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);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,16 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			AclResult	aclresult;
+
+			GetColumnEncryption(colDef->encryption, attr);
+			aclresult = object_aclcheck(ColumnEncKeyRelationId, attr->attcek, GetUserId(), ACL_USAGE);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attr->attcek, false));
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -2569,6 +2582,33 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								attributeName)));
 				def = (ColumnDef *) list_nth(inhSchema, exist_attno - 1);
 
+				/*
+				 * Check encryption parameter.  All parents must have the same
+				 * encryption settings for a column.
+				 */
+				if ((def->encryption && !attribute->attcek) ||
+					(!def->encryption && attribute->attcek))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && attribute->attcek)
+				{
+					/*
+					 * Merging the encryption properties of two encrypted
+					 * parent columns is not yet implemented.  Right now, this
+					 * would confuse the checks of the type etc. below (we
+					 * must check the physical and the real types against each
+					 * other, respectively), which might require a larger
+					 * restructuring.  For now, just give up here.
+					 */
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("multiple inheritance of encrypted columns is not implemented")));
+				}
+
 				/*
 				 * Must have the same type and typmod
 				 */
@@ -2661,6 +2701,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+														attribute->attusertypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -2950,6 +2996,34 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								 errdetail("%s versus %s", def->compression, newdef->compression)));
 				}
 
+				/*
+				 * Check encryption parameter.  All parents and children must
+				 * have the same encryption settings for a column.
+				 */
+				if ((def->encryption && !newdef->encryption) ||
+					(!def->encryption && newdef->encryption))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && newdef->encryption)
+				{
+					FormData_pg_attribute a, newa;
+
+					GetColumnEncryption(def->encryption, &a);
+					GetColumnEncryption(newdef->encryption, &newa);
+
+					if (a.atttypid != newa.atttypid ||
+						a.atttypmod != newa.atttypmod ||
+						a.attcek != newa.attcek)
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" has an encryption specification conflict",
+										attributeName)));
+				}
+
 				/*
 				 * Merge of NOT NULL constraints = OR 'em together
 				 */
@@ -6897,6 +6971,19 @@ 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);
+		aclresult = object_aclcheck(ColumnEncKeyRelationId, attribute.attcek, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attribute.attcek, false));
+	}
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attusertypid = 0;
+		attribute.attusertypmod = -1;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12728,6 +12815,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:
@@ -19328,3 +19418,110 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	List	   *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 = defGetQualifiedName(el);
+		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);
+
+			alg = get_cekalg_num(val);
+
+			if (!alg)
+				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 = get_cek_oid(cek, false);
+
+	attr->attcek = cekoid;
+	attr->attusertypid = attr->atttypid;
+	attr->attusertypmod = attr->atttypmod;
+
+	/* 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;
+
+	attr->atttypmod = alg;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	List	   *result;
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	char	   *nspname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attr->attcek));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attr->attcek);
+
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+	nspname = get_namespace_name(form->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(form->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->atttypmod))),
+									-1));
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index f0f2e07655..c65f6a4388 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/commands/view.c b/src/backend/commands/view.c
index ff98c773f5..171dca3803 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index dc8415a693..c81bc15128 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3983,6 +3983,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/gram.y b/src/backend/parser/gram.y
index a0138382a1..a6039878fa 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,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
@@ -592,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
@@ -690,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
@@ -708,13 +711,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	JOIN
 
-	KEY
+	KEY KEYS
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
 	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
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -1988,7 +1993,7 @@ CheckPointStmt:
 
 /*****************************************************************************
  *
- * DISCARD { ALL | TEMP | PLANS | SEQUENCES }
+ * DISCARD
  *
  *****************************************************************************/
 
@@ -2014,6 +2019,13 @@ DiscardStmt:
 					n->target = DISCARD_TEMP;
 					$$ = (Node *) n;
 				}
+			| DISCARD COLUMN ENCRYPTION KEYS
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+
+					n->target = DISCARD_COLUMN_ENCRYPTION_KEYS;
+					$$ = (Node *) n;
+				}
 			| DISCARD PLANS
 				{
 					DiscardStmt *n = makeNode(DiscardStmt);
@@ -3700,14 +3712,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;
@@ -3716,8 +3729,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;
@@ -3774,6 +3787,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4052,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6289,33 @@ 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
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (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; }
@@ -6289,6 +6335,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)); }
@@ -6800,6 +6850,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -7611,6 +7663,24 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| COLUMN ENCRYPTION KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CEK;
+					n->objs = $4;
+					$$ = n;
+				}
+			| COLUMN MASTER KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CMK;
+					n->objs = $4;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9140,6 +9210,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9817,6 +9907,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10148,6 +10258,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11304,6 +11432,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_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 any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16965,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16840,6 +17015,7 @@ unreserved_keyword:
 			| INVOKER
 			| ISOLATION
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -16854,6 +17030,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17514,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17404,6 +17582,7 @@ bare_label_keyword:
 			| ISOLATION
 			| JOIN
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -17425,6 +17604,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 2240284f21..a8a1855aa0 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,159 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			Expr	   *expr = te->expr;
+
+			/*
+			 * If it's a RelabelType, look inside.  (For encrypted columns,
+			 * this would typically be a typmod adjustment.)
+			 */
+			if (IsA(expr, RelabelType))
+				expr = castNode(RelabelType, expr)->arg;
+
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(expr, Param))
+			{
+				Param	   *p = (Param *) expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(expr, Var))
+			{
+				Var		   *v = (Var *) expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell   *lc;
+
+					foreach(lc, srcrte->values_lists)
+					{
+						List	   *values_list = lfirst_node(List, lc);
+						Expr	   *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, RelabelType))
+							value = castNode(RelabelType, value)->arg;
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach(lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f9218f48aa..8d902292cd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1035,8 +1035,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+												attribute->attusertypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2552327d90..79ec9c4d1f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2210,12 +2210,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 cab709b07b..b577557384 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -71,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"
 
@@ -1815,6 +1817,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 $%d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2560,6 +2572,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2618,11 +2632,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	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 = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				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 + 1)));
+
+			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->attusertypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x0001;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&row_description_buf, (int) pcekid);
+			pq_sendint32(&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 c7d9d96b45..180f86cb79 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,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,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 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,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 +2665,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 +2791,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;
@@ -2917,6 +2954,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_ALL:
 					tag = CMDTAG_DISCARD_ALL;
 					break;
+				case DISCARD_COLUMN_ENCRYPTION_KEYS:
+					tag = CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS;
+					break;
 				case DISCARD_PLANS:
 					tag = CMDTAG_DISCARD_PLANS;
 					break;
@@ -3063,6 +3103,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3736,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 8f7522d103..6aeb06f8fd 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -22,6 +22,8 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_foreign_data_wrapper.h"
 #include "catalog/pg_foreign_server.h"
@@ -101,6 +103,10 @@ static AclMode convert_table_priv_string(text *priv_type_text);
 static AclMode convert_sequence_priv_string(text *priv_type_text);
 static AttrNumber convert_column_name(Oid tableoid, text *column);
 static AclMode convert_column_priv_string(text *priv_type_text);
+static Oid	convert_column_encryption_key_name(text *cekname);
+static AclMode convert_column_encryption_key_priv_string(text *priv_type_text);
+static Oid	convert_column_master_key_name(text *cmkname);
+static AclMode convert_column_master_key_priv_string(text *priv_type_text);
 static Oid	convert_database_name(text *databasename);
 static AclMode convert_database_priv_string(text *priv_type_text);
 static Oid	convert_foreign_data_wrapper_name(text *fdwname);
@@ -800,6 +806,14 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			/* for backwards compatibility, grant some rights by default */
 			world_default = ACL_CREATE_TEMP | ACL_CONNECT;
@@ -911,6 +925,12 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 's':
 			objtype = OBJECT_SEQUENCE;
 			break;
+		case 'Y':
+			objtype = OBJECT_CEK;
+			break;
+		case 'y':
+			objtype = OBJECT_CMK;
+			break;
 		case 'd':
 			objtype = OBJECT_DATABASE;
 			break;
@@ -2915,6 +2935,384 @@ convert_column_priv_string(text *priv_type_text)
 }
 
 
+/*
+ * has_column_encryption_key_privilege variants
+ *		These are all named "has_column_encryption_key_privilege" at the SQL level.
+ *		They take various combinations of column encryption key name,
+ *		cek OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_encryption_key_privilege_name_name
+ *		Check user privileges on a column encryption key given
+ *		name username, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name
+ *		Check user privileges on a column encryption key given
+ *		text cekname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cekname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name_id
+ *		Check user privileges on a column encryption key given
+ *		name usename, column encryption key oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id
+ *		Check user privileges on a column encryption key given
+ *		column encryption key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cekid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_name
+ *		Check user privileges on a column encryption key given
+ *		roleid, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_id
+ *		Check user privileges on a column encryption key given
+ *		roleid, cek oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_encryption_key_privilege family.
+ */
+
+/*
+ * Given a CEK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_encryption_key_name(text *cekname)
+{
+	return get_cek_oid(textToQualifiedNameList(cekname), false);
+}
+
+/*
+ * convert_column_encryption_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_encryption_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_encryption_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_encryption_key_priv_map);
+}
+
+
+/*
+ * has_column_master_key_privilege variants
+ *		These are all named "has_column_master_key_privilege" at the SQL level.
+ *		They take various combinations of column master key name,
+ *		cmk OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_master_key_privilege_name_name
+ *		Check user privileges on a column master key given
+ *		name username, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name
+ *		Check user privileges on a column master key given
+ *		text cmkname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cmkname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name_id
+ *		Check user privileges on a column master key given
+ *		name usename, column master key oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id
+ *		Check user privileges on a column master key given
+ *		column master key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cmkid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_name
+ *		Check user privileges on a column master key given
+ *		roleid, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_id
+ *		Check user privileges on a column master key given
+ *		roleid, cmk oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_master_key_privilege family.
+ */
+
+/*
+ * Given a CMK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_master_key_name(text *cmkname)
+{
+	return get_cmk_oid(textToQualifiedNameList(cmkname), false);
+}
+
+/*
+ * convert_column_master_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_master_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_master_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_master_key_priv_map);
+}
+
+
 /*
  * has_database_privilege variants
  *		These are all named "has_database_privilege" at the SQL level.
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 5778e3f0ef..dd21fcf59e 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -679,6 +679,113 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	Node	   *escontext = fcinfo->context;
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c07382051d..7ad159110f 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
  *
@@ -3683,3 +3705,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+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;
+}
+
+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 77c2ba3f8f..d40af13efe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 94abede512..eb432260da 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"
@@ -219,6 +222,32 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAMENSP] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameNspIndexId,
+		KEY(Anum_pg_colenckey_cekname,
+			Anum_pg_colenckey_ceknamespace),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +262,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAMENSP] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameNspIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname,
+			Anum_pg_colmasterkey_cmknamespace),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 033647011b..4b6be68f27 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"
@@ -130,6 +132,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())
 	{
 		/*
@@ -237,6 +245,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,
@@ -297,7 +311,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 a2b74901e4..f35df16080 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -201,6 +203,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);
 
@@ -876,6 +884,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 079693585c..eeed0db211 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -484,6 +484,10 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('U', "USAGE");
 	}
+	else if (strcmp(type, "COLUMN ENCRYPTION KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "COLUMN MASTER KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "DATABASE") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,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 61ebb8fe85..bc303550fd 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3396,6 +3396,8 @@ _getObjectDescription(PQExpBuffer buf, const 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, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 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 4217908f84..f57bccee23 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,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);
@@ -393,6 +396,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},
@@ -685,6 +689,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");
 
@@ -1056,6 +1063,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"
@@ -5587,6 +5595,164 @@ 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_ceknamespace;
+	int			i_cekowner;
+	int			i_cekacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, cek.cekowner, cek.cekacl, acldefault('Y', cek.cekowner) AS acldefault\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_ceknamespace = PQfnumber(res, "ceknamespace");
+	i_cekowner = PQfnumber(res, "cekowner");
+	i_cekacl = PQfnumber(res, "cekacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cekacl));
+		cekinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cekinfo[i].dacl.privtype = 0;
+		cekinfo[i].dacl.initprivs = NULL;
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, 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].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			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);
+		if (!PQgetisnull(res, i, i_cekacl))
+			cekinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	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_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+	int			i_cmkacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, cmk.cmkowner, cmk.cmkrealm, cmk.cmkacl, acldefault('y', cmk.cmkowner) AS acldefault\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_cmknamespace = PQfnumber(res, "cmknamespace");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+	i_cmkacl = PQfnumber(res, "cmkacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		cmkinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cmkacl));
+		cmkinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cmkinfo[i].dacl.privtype = 0;
+		cmkinfo[i].dacl.initprivs = NULL;
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cmkacl))
+			cmkinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8202,6 +8368,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8261,8 +8430,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
+	appendPQExpBuffer(q, "SELECT\n"
 						 "a.attrelid,\n"
 						 "a.attnum,\n"
 						 "a.attname,\n"
@@ -8275,7 +8443,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attlen,\n"
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
+						 "pg_catalog.format_type(%s) AS atttypname,\n"
 						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
@@ -8284,7 +8452,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "' ' || pg_catalog.quote_literal(option_value) "
 						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
 						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+						 "), E',\n    ') AS attfdwoptions,\n",
+						 fout->remoteVersion >= 160000 ?
+						 "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END, CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END" :
+						 "a.atttypid, a.atttypmod");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -8310,10 +8481,23 @@ 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,
+						  "a.attcek,\n"
+						  "CASE a.atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "CASE WHEN a.atttypid IN (%u, %u) THEN a.atttypmod END AS attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID,
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcek,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
@@ -8338,6 +8522,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	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");
@@ -8398,6 +8585,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->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		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));
@@ -8425,6 +8615,22 @@ 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_attcek))
+			{
+				Oid		attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[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));
@@ -9943,6 +10149,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;
@@ -13357,6 +13569,141 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * 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",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_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,
+								  .namespace = cekinfo->dobj.namespace->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,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cekinfo->dobj.dumpId, InvalidDumpId, "COLUMN ENCRYPTION KEY",
+				qcekname, NULL, cekinfo->dobj.namespace->dobj.name,
+				cekinfo->rolname, &cekinfo->dacl);
+
+	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",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	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,
+								  .namespace = cmkinfo->dobj.namespace->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,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cmkinfo->dobj.dumpId, InvalidDumpId, "COLUMN MASTER KEY",
+				qcmkname, NULL, cmkinfo->dobj.namespace->dobj.name,
+				cmkinfo->rolname, &cmkinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15442,6 +15789,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[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_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -18010,6 +18373,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 cdca0b993d..d4a2e595d0 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,
@@ -332,6 +334,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	struct _CekInfo **attcek;
+	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 */
@@ -663,6 +668,32 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	struct _CmkInfo **cekcmks;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -683,6 +714,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -710,6 +743,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 8266c117a3..d3dacd39da 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 cd421c5944..6530fb81a2 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 187e4b8d07..9a8c032c97 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -719,6 +719,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, 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, },
@@ -1319,6 +1331,26 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1737,6 +1769,26 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3570,6 +3622,26 @@
 		unlike => { no_privs => 1, },
 	},
 
+	'GRANT USAGE ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
+	'GRANT USAGE ON COLUMN MASTER KEY cmk1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
 	'GRANT USAGE ON FOREIGN DATA WRAPPER dummy' => {
 		create_order => 85,
 		create_sql   => 'GRANT USAGE ON FOREIGN DATA WRAPPER dummy
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..0d6a46d24b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 99e28f607e..8f21fabd99 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1539,7 +1539,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;
@@ -1555,6 +1555,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;
@@ -1577,6 +1578,8 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attusertypid;
+	const char *attusertypmod;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1853,7 +1856,17 @@ 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)");
+	if (pset.sversion >= 160000)
+	{
+		attusertypid = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END";
+		attusertypmod = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END";
+	}
+	else
+	{
+		attusertypid = "a.atttypid";
+		attusertypmod = "a.atttypmod";
+	}
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, %s)", attusertypid, attusertypmod);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1866,7 +1879,8 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		attrdef_col = cols++;
 		attnotnull_col = cols++;
-		appendPQExpBufferStr(&buf, ",\n  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
+		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");
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
@@ -1918,6 +1932,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			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 ||
@@ -2041,6 +2067,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)
@@ -2133,6 +2161,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),
@@ -4486,6 +4525,152 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cekacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cmkacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cmk.oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 554fe86725..1cf8f72176 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1729966959 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,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 73d4b393bc..010bc5a6d5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,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 5a28b6f713..6736505c3a 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 8f12af799b..2f0ca71981 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -933,6 +933,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1223,6 +1237,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
@@ -1705,7 +1721,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1952,6 +1968,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_SCHEMA_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", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2894,6 +2926,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",
@@ -3605,7 +3657,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 	else if (Matches("DISCARD"))
-		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP");
+		COMPLETE_WITH("ALL", "COLUMN ENCRYPTION KEYS", "PLANS", "SEQUENCES", "TEMP");
 
 /* DO */
 	else if (Matches("DO"))
@@ -3619,6 +3671,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) ||
@@ -3931,6 +3984,8 @@ psql_completion(const char *text, int start, int end)
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"COLUMN ENCRYPTION KEY",
+											"COLUMN MASTER KEY",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4046,6 +4101,16 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+	/* Complete "GRANT/REVOKE * ON COLUMN ENCRYPTION|MASTER KEY *" with TO/FROM */
+	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
+			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny))
+	{
+		if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny, MatchAny))
+			COMPLETE_WITH("TO");
+		else
+			COMPLETE_WITH("FROM");
+	}
+
 	/* Complete "GRANT/REVOKE * ON FOREIGN DATA WRAPPER *" with TO/FROM */
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny))
diff --git a/src/common/Makefile b/src/common/Makefile
index 113029bf7b..73dce1150e 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..86c735878e
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,104 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		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";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index 41bd58ebdf..3695d3285b 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -4,6 +4,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 747ecb800d..4e384bbcdb 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,9 +20,13 @@ 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);
 
+extern void DiscardColumnEncryptionKeys(void);
+
 extern void debugStartup(DestReceiver *self, int operation,
 						 TupleDesc typeinfo);
 extern bool debugtup(TupleTableSlot *slot, DestReceiver *self);
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index ffd5e9dc82..eb59e73c0a 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/heap.h b/src/include/catalog/heap.h
index d01ab504b6..758696b539 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 3179be09d3..9e2c5256f7 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,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/namespace.h b/src/include/catalog/namespace.h
index f64a0ec26b..d0b9e8458d 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -115,6 +115,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c4d6adcd3e..c58b79e3a7 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 5b950129de..0e9e85ebf3 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 b561e17781..7910175a6a 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,17 @@ 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);
+
+	/*
+	 * User-visible type and typmod, currently used for encrypted columns.
+	 * These are only set to nondefault values if they are different from
+	 * atttypid and attypmod.
+	 */
+	Oid			attusertypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+	int32		attusertypmod BKI_DEFAULT(-1);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..c57fa18a27
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		cekacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_TOAST(pg_colenckey, 8263, 8264);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_nsp_index, 8242, ColumnEncKeyNameNspIndexId, on pg_colenckey using btree(cekname name_ops, ceknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /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-2023, 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);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..d3bfd36279
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+	aclitem		cmkacl[1] BKI_DEFAULT(_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_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, on pg_colmasterkey using btree(cmkname name_ops, cmknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c867d99563..ff06d52fd0 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 b2cdea66c4..114279fa64 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 91587b99d0..c21052a3f7 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 505595620e..b410388a42 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6355,6 +6355,14 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -7142,6 +7150,68 @@
   proname => 'fmgr_sql_validator', provolatile => 's', prorettype => 'void',
   proargtypes => 'oid', prosrc => 'fmgr_sql_validator' },
 
+{ oid => '8265',
+  descr => 'user privilege on column encryption key by username, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_encryption_key_privilege_name_name' },
+{ oid => '8266',
+  descr => 'user privilege on column encryption key by username, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_encryption_key_privilege_name_id' },
+{ oid => '8267',
+  descr => 'user privilege on column encryption key by user oid, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_encryption_key_privilege_id_name' },
+{ oid => '8268',
+  descr => 'user privilege on column encryption key by user oid, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_encryption_key_privilege_id_id' },
+{ oid => '8269',
+  descr => 'current user privilege on column encryption key by column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_encryption_key_privilege_name' },
+{ oid => '8270',
+  descr => 'current user privilege on column encryption key by column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_encryption_key_privilege_id' },
+
+{ oid => '8271',
+  descr => 'user privilege on column master key by username, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_master_key_privilege_name_name' },
+{ oid => '8272',
+  descr => 'user privilege on column master key by username, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_master_key_privilege_name_id' },
+{ oid => '8273',
+  descr => 'user privilege on column master key by user oid, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_master_key_privilege_id_name' },
+{ oid => '8274',
+  descr => 'user privilege on column master key by user oid, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_master_key_privilege_id_id' },
+{ oid => '8275',
+  descr => 'current user privilege on column master key by column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_master_key_privilege_name' },
+{ oid => '8276',
+  descr => 'current user privilege on column master key by column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_master_key_privilege_id' },
+
 { oid => '2250',
   descr => 'user privilege on database by username, database name',
   proname => 'has_database_privilege', provolatile => 's', prorettype => 'bool',
@@ -11939,4 +12009,37 @@
   proname => 'any_value_transfn', prorettype => 'anyelement',
   proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_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 92bcaf2c73..b9d3920a97 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,17 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+# Note: typstorage 'e' since compression is not useful for encrypted data
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 519e570c8c..3c7ab2a8fe 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 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, 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);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e7c2b91a58..11f88c59ce 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,4 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml>.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index ac6407e9f6..39a286e58a 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 371aa0ffc5..4253d531f8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -721,6 +721,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? */
@@ -757,11 +758,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1979,6 +1981,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2166,6 +2171,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
@@ -3644,6 +3674,7 @@ typedef struct CheckPointStmt
 typedef enum DiscardMode
 {
 	DISCARD_ALL,
+	DISCARD_COLUMN_ENCRYPTION_KEYS,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
 	DISCARD_TEMP
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..c6da932042 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)
@@ -228,6 +229,7 @@ PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL)
 PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("language", LANGUAGE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("large", LARGE_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -250,6 +252,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_param.h b/src/include/parser/parse_param.h
index d4865e50f6..26e1cd617a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..a5e61c9f5b 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)
@@ -130,6 +134,7 @@ PG_CMDTAG(CMDTAG_DECLARE_CURSOR, "DECLARE CURSOR", false, false, false)
 PG_CMDTAG(CMDTAG_DELETE, "DELETE", false, false, true)
 PG_CMDTAG(CMDTAG_DISCARD, "DISCARD", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
+PG_CMDTAG(CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS, "DISCARD COLUMN ENCRYPTION KEYS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false)
@@ -138,6 +143,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/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2..0c73022833 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -159,6 +159,8 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
+#define ACL_ALL_RIGHTS_CEK			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_CMK			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..1dac24e4b4 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,9 @@ 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 char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, 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 a443181d41..8ecacb29df 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d5d50ceab4..bf1d527c1a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAMENSP,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAMENSP,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c18e914228..10dfda8016 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 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
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 97e47f0585..24e326b448 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}
@@ -1412,6 +1420,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4056,6 +4086,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 0000000000..9baa47da13
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,839 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, 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 "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+#define pqResultAlloc(res, nBytes, isBinary) malloc(nBytes)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = 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;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								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);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05>
+ * for encrypting and decrypting column values.
+ */
+
+#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,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+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;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+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;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+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,
+					 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;
+	int64		al;
+	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;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	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;
+}
+
+/*
+ * Decrypt a column value
+ */
+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			iv_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 = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_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;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_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;
+	buf = pqResultAlloc(res, bufsize, false);
+	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;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#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)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "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 */
+
+/*
+ * Encrypt a column value
+ */
+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			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_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)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	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, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								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,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, 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, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, 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, const char *cmkfilename, 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 ec62550e38..4bd9e4e309 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,420 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+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 *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					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, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	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;
+
+	if (!conn->cmklookup || !conn->cmklookup[0])
+	{
+		libpq_append_conn_error(conn, "column master key lookup is not configured");
+		return NULL;
+	}
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', 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)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, PG_BINARY_R);
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+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
@@ -1251,13 +1668,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				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("protocol error: column encryption key associated with encrypted column was not sent by the server");
+					goto fail;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				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;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* 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;
@@ -1500,6 +1955,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1973,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2012,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2128,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2171,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2270,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2319,47 @@ PQsendQueryGuts(PGconn *conn,
 		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;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2378,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2396,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#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)
+				{
+					libpq_append_conn_error(conn, "protocol error: column encryption key associated with encrypted parameter was not sent by the server");
+					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
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2878,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4146,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4194,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)
 {
@@ -3762,6 +4390,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 8ab6a88416..617e641cf6 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);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -358,8 +366,21 @@ pqParseInput3(PGconn *conn)
 					}
 					break;
 				case 't':		/* Parameter Description */
-					if (getParamDescriptions(conn, msgLength))
-						return;
+					if (conn->error_result ||
+						(conn->result != NULL &&
+						 conn->result->resultStatus == PGRES_FATAL_ERROR))
+					{
+						/*
+						 * We've already choked for some reason.  Just discard
+						 * the data till we get to the end of the query.
+						 */
+						conn->inCursor += msgLength;
+					}
+					else
+					{
+						if (getParamDescriptions(conn, msgLength))
+							return;
+					}
 					break;
 				case 'D':		/* Data Row */
 					if (conn->result != NULL &&
@@ -547,6 +568,9 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		int			typlen;
 		int			atttypmod;
 		int			format;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGets(&conn->workBuffer, conn) ||
 			pqGetInt(&tableid, 4, conn) ||
@@ -561,6 +585,23 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, conn) ||
+				pqGetInt(&flags, 2, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -582,6 +623,8 @@ 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)
 			result->binary = 0;
@@ -685,10 +728,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, 4, 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! */
@@ -1468,6 +1532,92 @@ 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);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	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;
+	int			ret;
+
+	/* 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, 4, 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 */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2436,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 abaab6a073..5c2793a5eb 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);
+			pqTraceOutputInt32(f, message, cursor, false);
+			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,12 @@ 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);
+			pqTraceOutputInt32(f, message, cursor, false);
+			pqTraceOutputInt16(f, message, cursor);
+		}
 	}
 }
 
@@ -514,6 +530,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);
+	pqTraceOutputInt32(f, message, cursor, false);
+	len = pqTraceOutputInt32(f, message, cursor, false);
+	pqTraceOutputNchar(f, len, message, cursor);
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -647,10 +687,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 +707,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 f3d9220496..cf339a3e53 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,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,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 +559,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 d7ec5ed429..05b5891ffe 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 */
 
 	/* 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);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 573fd9b6ea..a6e6b2ada9 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,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
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': get_option('ssl')},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index b6ed455183..9aef1c7a29 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -3,6 +3,7 @@ CATALOG_NAME     = libpq
 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 \
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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 b2a4b06fd2..87d2808b52 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c9a3868053 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),column_encryption examples kerberos icu ldap ssl)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..764cadf550
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, 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
+
+export OPENSSL
+
+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 0000000000..84cfa84e12
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,23 @@
+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',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..8fa253524d
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,257 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+my $perlbin = $^X;
+$perlbin =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$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"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	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}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# 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\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[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 %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = qq{*=run:"$perlbin" ./test_run_decrypt.pl %k %a %p};
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$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\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$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 update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$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', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+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');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$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 tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$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 second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..14eafb8ec9
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, 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 $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# 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, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+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, algorithm = '$cmkalg', 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 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	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;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	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 = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	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
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/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-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $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}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..d55ddb1ab7 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,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 0000000000..df47e36280
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,451 @@
+\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;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+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       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  permission denied for column master key cmk1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+ERROR:  permission denied for column encryption key cek1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column 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 in schema "public"
+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 in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION '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 SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf..8dbb4a847a 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -101,6 +103,7 @@ BEGIN
         ('materialized view'), ('foreign table'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
+        ('column encryption key'), ('column encryption key data'), ('column master key'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
         ('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
         ('text search parser'), ('text search dictionary'),
@@ -201,6 +204,24 @@ WARNING:  error for cast,{addr_nsp,zwei},{}: name list length must be exactly 1
 WARNING:  error for cast,{addr_nsp,zwei},{integer}: name list length must be exactly 1
 WARNING:  error for cast,{eins,zwei,drei},{}: name list length must be exactly 1
 WARNING:  error for cast,{eins,zwei,drei},{integer}: name list length must be exactly 1
+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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+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},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 WARNING:  error for table constraint,{eins},{}: must specify relation and object name
 WARNING:  error for table constraint,{eins},{integer}: must specify relation and object name
 WARNING:  error for table constraint,{addr_nsp,zwei},{}: relation "addr_nsp" does not exist
@@ -409,6 +430,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}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,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
@@ -547,6 +576,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
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|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 215eb899be..226f5e404e 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 {attusertypid} => 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,9 @@ 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 {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {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 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ 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)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,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
@@ -872,6 +876,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
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 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,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 15e015b3d6..577a6d4b2e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 427429975e..57c7d2bec3 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..8d6320015c
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,297 @@
+\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;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+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
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+
+
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+
+
+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 SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+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 ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..61828613d9 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -93,6 +95,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
         ('materialized view'), ('foreign table'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
+        ('column encryption key'), ('column encryption key data'), ('column master key'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
         ('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
         ('text search parser'), ('text search dictionary'),
@@ -174,6 +177,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}', '{}'),
@@ -228,6 +234,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;
 
@@ -247,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
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 36ea345f8fa616fd9b40576310e54145aa70c1a1
-- 
2.39.2

#72Mark Dilger
mark.dilger@enterprisedb.com
In reply to: Peter Eisentraut (#71)
Re: Transparent column encryption

On Mar 9, 2023, at 11:18 PM, Peter Eisentraut <peter.eisentraut@enterprisedb.com> wrote:

Here is an updated patch.

Thanks, Peter. The patch appears to be in pretty good shape, but I have a few comments and concerns.

CEKIsVisible() and CMKIsVisible() are obviously copied from TSParserIsVisible(), and the code comments weren't fully updated. Specifically, the phrase "hidden by another parser of the same name" should be updated to not mention "parser".

Why does get_cmkalg_name() return the string "unspecified" for PG_CMK_UNSPECIFIED, but the next function get_cmkalg_jwa_name() returns NULL for PG_CMK_UNSPECIFIED? It seems they would both return NULL, or both return "unspecified". If there's a reason for the divergence, could you add a code comment to clarify? BTW, get_cmkalg_jwa_name() has no test coverage.

Looking further at code coverage, the new conditional in printsimple_startup() is never tested with (MyProcPort->column_encryption_enabled), so the block is never entered. This would seem to be a consequence of backends like walsender not using column encryption, which is not terribly surprising, but it got me wondering if you had a particular client use case in mind when you added this block?

The new function pg_encrypted_in() appears totally untested, but I have to wonder if that's because we're not round-trip testing pg_dump with column encryption...? The code coverage in pg_dump looks fairly decent, but some column encryption code is not covered.


Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#73Andres Freund
andres@anarazel.de
In reply to: Peter Eisentraut (#71)
Re: Transparent column encryption

Hi,

On 2023-03-10 08:18:34 +0100, Peter Eisentraut wrote:

Here is an updated patch. I have done some cosmetic polishing and fixed a
minor Windows-related bug.

In my mind, the patch is complete.

If someone wants to do some in-depth code review, I suggest focusing on the
following files:

* src/backend/access/common/printtup.c

Have you done benchmarks of some simple workloads to verify this doesn't cause
slowdowns (when not using encryption, obviously)? printtup.c is a performance
sensitive portion for simple queries, particularly when they return multiple
columns.

And making tupledescs even wider is likely to have some price, both due to the
increase in memory usage, and due to the lower cache density - and that's code
where we're already hurting noticeably.

Greetings,

Andres Freund

#74Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Mark Dilger (#72)
1 attachment(s)
Re: Transparent column encryption

On 11.03.23 19:08, Mark Dilger wrote:

CEKIsVisible() and CMKIsVisible() are obviously copied from TSParserIsVisible(), and the code comments weren't fully updated. Specifically, the phrase "hidden by another parser of the same name" should be updated to not mention "parser".

fixed

Why does get_cmkalg_name() return the string "unspecified" for PG_CMK_UNSPECIFIED, but the next function get_cmkalg_jwa_name() returns NULL for PG_CMK_UNSPECIFIED? It seems they would both return NULL, or both return "unspecified". If there's a reason for the divergence, could you add a code comment to clarify?

Added a comment.

BTW, get_cmkalg_jwa_name() has no test coverage.

Ok, I'll look into it.

Looking further at code coverage, the new conditional in printsimple_startup() is never tested with (MyProcPort->column_encryption_enabled), so the block is never entered. This would seem to be a consequence of backends like walsender not using column encryption, which is not terribly surprising, but it got me wondering if you had a particular client use case in mind when you added this block?

AFAICT, the relationship between printsimple.c and the replicaton
protocol is not actually firmly defined anywhere, it just happens that
they are used together. So I feel the column encryption mode needs to
be supported, technically, even if nothing is using it right now.

The new function pg_encrypted_in() appears totally untested, but I have to wonder if that's because we're not round-trip testing pg_dump with column encryption...? The code coverage in pg_dump looks fairly decent, but some column encryption code is not covered.

I have added test coverage for pg_encrypted_in() (via a COPY round-trip
test in under src/test/column_encryption), as well as additional
coverage in pg_dump and some DDL commands. I didn't find any obvious
gaps in test coverage elsewhere.

Attachments:

v19-0001-Automatic-client-side-column-level-encryption.patchtext/plain; charset=UTF-8; name=v19-0001-Automatic-client-side-column-level-encryption.patchDownload
From a205416fa831814764bb399bfade5fc7c3e57eb5 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Mon, 13 Mar 2023 21:08:56 +0100
Subject: [PATCH v19] Automatic client-side column-level encryption

This feature enables the automatic 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 asymmetric 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
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.

To get automatically encrypted data into the database (as opposed to
reading it out), it is required to use protocol-level prepared
statements (i.e., extended query).  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.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this if the
\bind command is used.

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.

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                    | 317 +++++++
 doc/src/sgml/charset.sgml                     |  10 +
 doc/src/sgml/datatype.sgml                    |  55 ++
 doc/src/sgml/ddl.sgml                         | 444 +++++++++
 doc/src/sgml/func.sgml                        |  60 ++
 doc/src/sgml/glossary.sgml                    |  26 +
 doc/src/sgml/libpq.sgml                       | 322 +++++++
 doc/src/sgml/protocol.sgml                    | 467 ++++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |  10 +
 .../ref/create_column_encryption_key.sgml     | 173 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  55 +-
 doc/src/sgml/ref/discard.sgml                 |  14 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/grant.sgml                   |  12 +-
 doc/src/sgml/ref/pg_dump.sgml                 |  42 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   8 +
 src/backend/access/common/printtup.c          | 237 ++++-
 src/backend/access/common/tupdesc.c           |  12 +
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/catalog/Makefile                  |   3 +-
 src/backend/catalog/aclchk.c                  |  60 ++
 src/backend/catalog/dependency.c              |  18 +
 src/backend/catalog/heap.c                    |  42 +-
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 288 ++++++
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  17 +
 src/backend/commands/colenccmds.c             | 439 +++++++++
 src/backend/commands/createas.c               |  32 +
 src/backend/commands/discard.c                |   8 +-
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |  12 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 217 +++++
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  20 +
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 200 ++++-
 src/backend/parser/parse_param.c              | 157 ++++
 src/backend/parser/parse_utilcmd.c            |  12 +-
 src/backend/postmaster/postmaster.c           |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  56 ++
 src/backend/utils/adt/acl.c                   | 398 +++++++++
 src/backend/utils/adt/varlena.c               | 107 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/cache/syscache.c            |  42 +
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/dumputils.c                   |   4 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 377 +++++++-
 src/bin/pg_dump/pg_dump.h                     |  35 +
 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              |  72 ++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 191 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  69 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 107 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   4 +
 src/include/catalog/dependency.h              |   3 +
 src/include/catalog/heap.h                    |   1 +
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   5 +
 src/include/catalog/pg_attribute.h            |  11 +
 src/include/catalog/pg_colenckey.h            |  46 +
 src/include/catalog/pg_colenckeydata.h        |  46 +
 src/include/catalog/pg_colmasterkey.h         |  47 +
 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               | 103 +++
 src/include/catalog/pg_type.dat               |  13 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   2 +
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/nodes/parsenodes.h                |  41 +-
 src/include/parser/kwlist.h                   |   3 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   7 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/include/utils/syscache.h                  |   6 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 839 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 671 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 157 +++-
 src/interfaces/libpq/fe-trace.c               |  56 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  36 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   1 +
 src/interfaces/libpq/t/003_encrypt.pl         |  70 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  23 +
 .../t/001_column_encryption.pl                | 271 ++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 112 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  58 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 539 +++++++++++
 src/test/regress/expected/object_address.out  |  35 +
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 371 ++++++++
 src/test/regress/sql/object_address.sql       |  11 +
 src/test/regress/sql/type_sanity.sql          |   2 +
 144 files changed, 10592 insertions(+), 84 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/common/colenc.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/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/003_encrypt.pl
 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 2df6559acc..3a5f2c254c 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; see <xref linkend="ddl-column-encryption"/>
+     </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; see <xref linkend="ddl-column-encryption"/>
+     </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 746baf5053..54c5199e0f 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,44 @@ <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>attusertypid</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.  If the column is not
+       encrypted, then 0.  For encrypted columns, the field
+       <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.
+       </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type
+       modifier (analogous to <structfield>atttypmod</structfield>) that is
+       reported to the client.  If the column is not encrypted, then -1.  For
+       encrypted columns, the field <structfield>atttypmod</structfield>)
+       contains 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>
@@ -2476,6 +2529,270 @@ <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>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </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>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </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>int4</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>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </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>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml
index 12fabb7372..87ddf72edf 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -1746,6 +1746,16 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When automatic client-side column-level encryption is used, then no
+     encoding conversion is possible.  (The encoding conversion happens on the
+     server, and the server cannot look inside any encrypted column values.)
+     If automatic client-side column-level encryption is enabled for a
+     session, then the server enforces that the client encoding matches the
+     server encoding, and any attempts to change the client encoding will be
+     rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 467b49b199..67fce16872 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5360,4 +5360,59 @@ <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
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support automatic client-side column-level encryption
+    or have disabled it will see the encrypted values in this format.  Clients
+    that support automatic client-side column-level encryption will not see
+    these types in result sets, as the protocol layer will translate them back
+    to the 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 5179125510..65514e119f 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1238,6 +1238,440 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   With <firstterm>automatic client-side column-level encryption</firstterm>,
+   columns can be stored encrypted in the database.  The encryption and
+   decryption happens automatically 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 Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   Automatic client-side column-level 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 asymmetric 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 from making
+   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 automatic client-side column-level
+   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 automatic client-side column-level
+       encryption.  Not all client libraries do.  Furthermore, the client
+       library might require that automatic client-side column-level
+       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>
+    would 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 employees (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This would leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of client-side column-level encryption.
+    (And even ignoring that, it could not work because the server does not
+    have access to the keys to perform the 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>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</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>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2>
+   <title>Setting up Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   The steps to set up automatic client-side column-level 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"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</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, algorithm = 'RSAES_OAEP_SHA_1', 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 automatic client-side column-level 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 Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use automatic client-side column-level 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 transport 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 allows 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>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation; for
+    <application>libpq</application>, see <xref
+    linkend="libpq-connect-column-encryption"/>).
+   </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, 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.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <tip>
+    <para>
+     One might be inclined to think of the client-side column-level encryption
+     feature as a mechanism for application writers and users to protect
+     themselves against an <quote>evil DBA</quote>, but that is not the
+     intended purpose.  Rather, it is (also) a tool for the DBA to control
+     which data they do not want (in plaintext) on the server.
+    </para>
+   </tip>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on 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>
 
@@ -1986,6 +2420,14 @@ <title>Privileges</title>
        server.  Grantees may also create, alter, or drop their own user
        mappings associated with that server.
       </para>
+      <para>
+       For column master keys, allows the creation of column encryption keys
+       using the master key.
+      </para>
+      <para>
+       For column encryption keys, allows the use of the key in the creation
+       of table columns.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2152,6 +2594,8 @@ <title>ACL Privilege Abbreviations</title>
       <entry><literal>USAGE</literal></entry>
       <entry><literal>U</literal></entry>
       <entry>
+       <literal>COLUMN ENCRYPTION KEY</literal>,
+       <literal>COLUMN MASTER KEY</literal>,
        <literal>DOMAIN</literal>,
        <literal>FOREIGN DATA WRAPPER</literal>,
        <literal>FOREIGN SERVER</literal>,
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 9c6107f960..88e2b30f13 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -22859,6 +22859,40 @@ <title>Access Privilege Inquiry Functions</title>
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_encryption_key_privilege</primary>
+        </indexterm>
+        <function>has_column_encryption_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cek</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column encryption key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_master_key_privilege</primary>
+        </indexterm>
+        <function>has_column_master_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cmk</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column master key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
@@ -23349,6 +23383,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index 7c01a541fe..818038d860 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -389,6 +389,32 @@ <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 automatic
+     client-side column-level encryption (<xref
+     linkend="ddl-column-encryption"/>).  Column encryption keys are stored in
+     the database encrypted by another key, the <glossterm
+     linkend="glossary-column-master-key">column master key</glossterm>.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt <glossterm
+     linkend="glossary-column-encryption-key">column encryption
+     keys</glossterm>.  (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 3ccd8ff942..26cab10104 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1964,6 +1964,141 @@ <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 <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, this enables automatic client-side column-level
+        encryption for the connection.  If encrypted columns are queried and
+        this is not enabled, the encrypted values are 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>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </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>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </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 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 --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
    </para>
   </sect2>
@@ -2864,6 +2999,32 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            half-byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this half-byte to one to force
+            encryption.  For example, use the C code literal
+            <literal>0x10</literal> to specify text format with forced
+            encryption.  If the array pointer is null then encryption is not
+            forced for any parameter.
+           </para>
+
+           <para>
+            Parameters corresponding to encrypted columns must be passed in
+            text format.  Specifying binary format for such a parameter will
+            result in an error.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter 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 compromised 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.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -2876,6 +3037,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3028,6 +3196,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</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 *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> 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>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -3878,6 +4084,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>
 
@@ -4059,6 +4287,31 @@ <title>Retrieving Query Result Information</title>
      </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-PQexecPreparedDescribed"/> 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>
 
@@ -4584,6 +4837,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>, and
    <xref linkend="libpq-PQsendDescribePortal"/>,
    which can be used with <xref linkend="libpq-PQgetResult"/> to duplicate
@@ -4591,6 +4845,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>, and
    <xref linkend="libpq-PQdescribePortal"/>
    respectively.
@@ -4647,6 +4902,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -4701,6 +4963,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</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 PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> 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-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -4751,6 +5052,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>, or
        <xref linkend="libpq-PQpipelineSync"/>
@@ -7784,6 +8086,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 73b7f4432f..c43c3051c3 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1109,6 +1109,76 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-column-encryption">
+   <title>Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    Automatic client-side column-level 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 automatic client-side column-level 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 automatic column-level 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, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column-level encryption protocol option is enabled.  That way, a client
+    library only needs to support decrypting data sent in binary and does not
+    have to support decoding the text format of the encryption-related types
+    (see <xref linkend="datatype-encrypted"/>).
+   </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 automatic client-side column-level 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 automatic client-side column-level
+    encryption are described in <xref
+    linkend="protocol-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -3841,6 +3911,16 @@ <title>Message Formats</title>
          The parameter format codes.  Each must presently be
          zero (text) or one (binary).
         </para>
+
+        <para>
+         If the protocol extension <literal>_pq_.column_encryption</literal>
+         is enabled (see <xref linkend="protocol-flow-column-encryption"/>),
+         then the second-least-significant half-byte is set to one if the
+         parameter was encrypted by the client.  (So, for example, to send an
+         encrypted value in binary, the field is set to 0x11 in total.)  This
+         is used by the server to check that a parameter that was required to
+         be encrypted was actually encrypted.
+        </para>
        </listitem>
       </varlistentry>
 
@@ -4061,6 +4141,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-flow-column-encryption"/>.)
+     </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>Int32</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-flow-column-encryption"/>.)
+     </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>
@@ -5164,6 +5378,45 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), 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>Int32</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 parameter is to be
+         encrypted and bit 0x0001 is set, the column underlying the parameter
+         uses deterministic encryption, otherwise randomized encryption.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5552,6 +5805,50 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), 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>Int32</term>
+       <listitem>
+        <para>
+         If the field is 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 field is encrypted and
+         bit 0x0001 is set, the field uses deterministic encryption, otherwise
+         randomized encryption.
+        </para>
+        <!--
+            This is not really useful here, but it keeps alignment with
+            ParameterDescription.  Future flags might be useful in both
+            places.
+        -->
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7377,6 +7674,176 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-column-encryption-crypto">
+  <title>Automatic Client-side Column-level Encryption Cryptography</title>
+
+  <para>
+   This section describes the cryptographic operations used by the automatic
+   client-side column-level encryption functionality.  A client that supports
+   this functionality 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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://tools.ietf.org/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://tools.ietf.org/html/rfc8017">RFC 8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<ulink
+       url="https://tools.ietf.org/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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</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>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</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>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</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>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</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>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</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 version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </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 IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </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 54b5f22d6e..a730e5d650 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 0000000000..655e1e00d8
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+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 }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</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
+   schema.  (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 (optionally schema-qualified) 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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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 0000000000..7f0e656ef0
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+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> ( REALM = <replaceable>realm</replaceable> )
+
+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 }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </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 schema.
+   (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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b13..1caf9bfa56 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index c25b52d0cb..ebcbf5d00a 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -555,6 +555,16 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support automatic client-side
+    column-level encryption or decryption; its input or output data will
+    always be the ciphertext.  This is usually suitable for backups (see also
+    <xref linkend="app-pgdump"/>).  If automatic client-side encryption or
+    decryption is wanted, <command>INSERT</command> and
+    <command>SELECT</command> need to be used instead to write and read the
+    data.
+   </para>
+
  </refsect1>
 
  <refsect1>
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 0000000000..65534fb03f
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,173 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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.  You must have <literal>USAGE</literal> privilege on the
+      column master 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>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></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.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </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,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    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,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    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 0000000000..6aaa1088d1
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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 a03dee4afe..d1549c7f45 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 | DEFAULT } ] [ 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 | DEFAULT } ] [ 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> ... ] }
     [, ... ]
@@ -87,7 +87,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -351,6 +351,47 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables automatic client-side column-level 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.  You must have <literal>USAGE</literal> privilege
+          on the column encryption key.
+         </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 id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -704,6 +745,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</literal></term>
         <listitem>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523ca..6a94706ef7 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@
 
  <refsynopsisdiv>
 <synopsis>
-DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
+DISCARD { ALL | COLUMN ENCRYPTION KEYS | PLANS | SEQUENCES | TEMPORARY | TEMP }
 </synopsis>
  </refsynopsisdiv>
 
@@ -42,6 +42,17 @@ <title>Parameters</title>
 
   <variablelist>
 
+   <varlistentry>
+    <term><literal>COLUMN ENCRYPTION KEYS</literal></term>
+    <listitem>
+     <para>
+      Discards knowledge about which column encryption keys and column master
+      keys have been sent to the client in this session.  (They will
+      subsequently be re-sent as required.)
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>PLANS</literal></term>
     <listitem>
@@ -93,6 +104,7 @@ <title>Parameters</title>
 DISCARD PLANS;
 DISCARD TEMP;
 DISCARD SEQUENCES;
+DISCARD COLUMN ENCRYPTION KEYS;
 </programlisting></para>
     </listitem>
    </varlistentry>
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 0000000000..f2ac1beb08
--- /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 (optionally schema-qualified) 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 0000000000..fae95e09d1
--- /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 (optionally schema-qualified) 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/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 35bf0332c8..f712f8e9e4 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -46,6 +46,16 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN ENCRYPTION KEY <replaceable>cek_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN MASTER KEY <replaceable>cmk_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON DOMAIN <replaceable>domain_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -513,7 +523,7 @@ <title>Compatibility</title>
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
+    Privileges on databases, tablespaces, schemas, keys, languages, and
     configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index 334e4b7fd1..5412ea8666 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -716,6 +716,48 @@ <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 turns on the column encryption connection option in
+        <application>libpq</application> (see <xref
+        linkend="libpq-connect-column-encryption"/>).  Column master key
+        lookup must be configured by the user, either through a connection
+        option or an environment setting (see <xref
+        linkend="libpq-connect-cmklookup"/>).
+       </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.  (But then it is recommended
+        to not do this on the same host as the server, to avoid exposing
+        unencrypted data that is meant to be kept encrypted on the server.)
+        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 ... \bind ... \g 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 e62d05e5ab..4bf60c729f 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 7b8ae9fac3..b6115e433e 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1420,6 +1420,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4029,6 +4057,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <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 id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index e11b4b6130..c898997915 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 ef818228ac..c5894893eb 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,12 @@ 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_sendint32(&buf, 0);	/* CEK alg */
+			pq_sendint16(&buf, 0);	/* flags */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index 72faeb5dfa..2d627b6f47 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,166 @@ 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;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	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_sendint32(&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;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	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);
+}
+
+void
+DiscardColumnEncryptionKeys(void)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -167,6 +342,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 +359,18 @@ 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(int32)		/* attencalg */
+			   + sizeof(int16));	/* flags */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -200,6 +380,9 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
+		int16		flags = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -231,6 +414,31 @@ 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->attusertypid;
+			atttypmod = orig_att->attusertypmod;
+			attcekid = orig_att->attcek;
+			attencalg = orig_att->atttypmod;
+			if (orig_att->atttypid == PG_ENCRYPTED_DETOID)
+				flags |= 0x0001;
+			ReleaseSysCache(tp);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -238,6 +446,12 @@ 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_writeint32(buf, attencalg);
+			pq_writeint16(buf, flags);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -271,6 +485,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index 72a2c3d3db..f86ba299c3 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->attusertypid != attr2->attusertypid)
+			return false;
+		if (attr1->attusertypmod != attr2->attusertypmod)
+			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->attusertypid = 0;
+	att->attusertypmod = -1;
 	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->attusertypid = 0;
+	att->attusertypmod = -1;
 	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 24bab58499..cc48069932 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 a60107bf94..7b9575635b 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 c4232344aa..a3547c6cae 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -33,7 +33,9 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.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"
@@ -247,6 +249,12 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_SEQUENCE:
 			whole_mask = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			whole_mask = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			whole_mask = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			whole_mask = ACL_ALL_RIGHTS_DATABASE;
 			break;
@@ -473,6 +481,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
 			errormsg = gettext_noop("invalid privilege type %s for sequence");
 			break;
+		case OBJECT_CEK:
+			all_privileges = ACL_ALL_RIGHTS_CEK;
+			errormsg = gettext_noop("invalid privilege type %s for column encryption key");
+			break;
+		case OBJECT_CMK:
+			all_privileges = ACL_ALL_RIGHTS_CMK;
+			errormsg = gettext_noop("invalid privilege type %s for column master key");
+			break;
 		case OBJECT_DATABASE:
 			all_privileges = ACL_ALL_RIGHTS_DATABASE;
 			errormsg = gettext_noop("invalid privilege type %s for database");
@@ -597,6 +613,12 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_SEQUENCE:
 			ExecGrant_Relation(istmt);
 			break;
+		case OBJECT_CEK:
+			ExecGrant_common(istmt, ColumnEncKeyRelationId, ACL_ALL_RIGHTS_CEK, NULL);
+			break;
+		case OBJECT_CMK:
+			ExecGrant_common(istmt, ColumnMasterKeyRelationId, ACL_ALL_RIGHTS_CMK, NULL);
+			break;
 		case OBJECT_DATABASE:
 			ExecGrant_common(istmt, DatabaseRelationId, ACL_ALL_RIGHTS_DATABASE, NULL);
 			break;
@@ -676,6 +698,26 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 				objects = lappend_oid(objects, relOid);
 			}
 			break;
+		case OBJECT_CEK:
+			foreach(cell, objnames)
+			{
+				List	   *cekname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cek_oid(cekname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
+		case OBJECT_CMK:
+			foreach(cell, objnames)
+			{
+				List	   *cmkname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cmk_oid(cmkname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
 		case OBJECT_DATABASE:
 			foreach(cell, objnames)
 			{
@@ -2693,6 +2735,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("permission denied for column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("permission denied for column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -2798,6 +2846,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:
@@ -2828,6 +2877,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;
@@ -2938,6 +2993,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:
@@ -3019,6 +3075,10 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
+		case OBJECT_CEK:
+			return object_aclmask(ColumnEncKeyRelationId, object_oid, roleid, mask, how);
+		case OBJECT_CMK:
+			return object_aclmask(ColumnMasterKeyRelationId, object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
 		case OBJECT_FUNCTION:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index f8a136ba0a..cab6cfd140 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 */
@@ -1493,6 +1499,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 4f006820b8..df282c796f 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,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_foreign_table.h"
@@ -511,7 +512,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+						   /*
+							* Allow encrypted types if CEK has been provided,
+							* which means this type has been internally
+							* generated.  We don't want to allow explicitly
+							* using these types.
+							*/
+						   (TupleDescAttr(tupdesc, i)->attcek ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -653,6 +661,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -749,6 +772,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_attusertypid - 1] = ObjectIdGetDatum(attrs->attusertypid);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attusertypmod - 1] = Int32GetDatum(attrs->attusertypmod);
 		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)
@@ -840,6 +866,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (OidIsValid(tupdesc->attrs[i].attcek))
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 tupdesc->attrs[i].attcek);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (OidIsValid(tupdesc->attrs[i].attusertypid))
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 tupdesc->attrs[i].attusertypid);
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 14e57adee2..153a048e8e 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -26,6 +26,8 @@
 #include "catalog/dependency.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -1997,6 +1999,254 @@ OpfamilyIsVisible(Oid opfid)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified CEK name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified CMK name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4567,6 +4817,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(OpfamilyIsVisible(oid));
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 2f688166e1..6c4ab9ac59 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,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		Anum_pg_colenckey_cekowner,
+		Anum_pg_colenckey_cekacl,
+		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,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		Anum_pg_colmasterkey_cmkowner,
+		Anum_pg_colmasterkey_cmkacl,
+		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
@@ -1029,6 +1086,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1108,6 +1175,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, 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));
@@ -2311,6 +2393,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2367,6 +2451,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2489,6 +2574,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2575,6 +2662,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3040,6 +3128,92 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				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);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				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 data of %s for %s"),
+								 getObjectDescription(&(ObjectAddress){ColumnEncKeyRelationId, cekdata->ckdcekid}, false),
+								 getObjectDescription(&(ObjectAddress){ColumnMasterKeyRelationId, cekdata->ckdcmkid}, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  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_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case OCLASS_CONSTRAINT:
 			{
 				HeapTuple	conTup;
@@ -4440,6 +4614,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);
@@ -4905,6 +5091,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case OCLASS_CEK:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, 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",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case OCLASS_CMK:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, 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 48f7348f91..69f6175c60 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 bea51b3af1..59c23e9ef8 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"
@@ -118,6 +120,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -379,6 +387,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:
@@ -527,6 +537,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -643,6 +655,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:
@@ -876,6 +891,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 0000000000..3ada6d5aeb
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,439 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2023, 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/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.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, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	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)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	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")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	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, int 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] = Int32GetDatum(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)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach (lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+	nulls[Anum_pg_colenckey_cekacl - 1] = true;
+
+	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 (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(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);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+		AclResult	aclresult;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		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\"",
+						   NameListToString(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)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	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);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+	nulls[Anum_pg_colmasterkey_cmkacl - 1] = true;
+
+	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;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	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)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index d6c6d514f3..9685b7886a 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -50,6 +50,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -205,6 +206,26 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				col->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				col->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -514,6 +535,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 296dc82d2e..86d22ca065 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/printtup.h"
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "commands/async.h"
@@ -25,7 +26,7 @@
 static void DiscardAll(bool isTopLevel);
 
 /*
- * DISCARD { ALL | SEQUENCES | TEMP | PLANS }
+ * DISCARD
  */
 void
 DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
@@ -36,6 +37,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			DiscardAll(isTopLevel);
 			break;
 
+		case DISCARD_COLUMN_ENCRYPTION_KEYS:
+			DiscardColumnEncryptionKeys();
+			break;
+
 		case DISCARD_PLANS:
 			ResetPlanCache();
 			break;
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	DiscardColumnEncryptionKeys();
 }
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 82bda15889..b4c681005a 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -276,6 +276,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -503,6 +517,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index d4b00d1a82..4c1628cf7b 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 42cced9ebe..4b5ac30441 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 7ff16e3276..93d61c96ad 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 3e2c5f797c..ec8eda5b09 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"
@@ -61,6 +62,7 @@
 #include "commands/trigger.h"
 #include "commands/typecmds.h"
 #include "commands/user.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -637,6 +639,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(ParseState *pstate, const List *coldefencryption, Form_pg_attribute attr);
 
 
 /* ----------------------------------------------------------------
@@ -936,6 +939,16 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			attr->attcompression = GetAttributeCompression(attr->atttypid,
 														   colDef->compression);
 
+		if (colDef->encryption)
+		{
+			AclResult	aclresult;
+
+			GetColumnEncryption(NULL, colDef->encryption, attr);
+			aclresult = object_aclcheck(ColumnEncKeyRelationId, attr->attcek, GetUserId(), ACL_USAGE);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attr->attcek, false));
+		}
+
 		if (colDef->storage_name)
 			attr->attstorage = GetAttributeStorage(attr->atttypid, colDef->storage_name);
 	}
@@ -2569,6 +2582,33 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								attributeName)));
 				def = (ColumnDef *) list_nth(inhSchema, exist_attno - 1);
 
+				/*
+				 * Check encryption parameter.  All parents must have the same
+				 * encryption settings for a column.
+				 */
+				if ((def->encryption && !attribute->attcek) ||
+					(!def->encryption && attribute->attcek))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && attribute->attcek)
+				{
+					/*
+					 * Merging the encryption properties of two encrypted
+					 * parent columns is not yet implemented.  Right now, this
+					 * would confuse the checks of the type etc. below (we
+					 * must check the physical and the real types against each
+					 * other, respectively), which might require a larger
+					 * restructuring.  For now, just give up here.
+					 */
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("multiple inheritance of encrypted columns is not implemented")));
+				}
+
 				/*
 				 * Must have the same type and typmod
 				 */
@@ -2661,6 +2701,12 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 				def->colname = pstrdup(attributeName);
 				def->typeName = makeTypeNameFromOid(attribute->atttypid,
 													attribute->atttypmod);
+				if (type_is_encrypted(attribute->atttypid))
+				{
+					def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+														attribute->attusertypmod);
+					def->encryption = makeColumnEncryption(attribute);
+				}
 				def->inhcount = 1;
 				def->is_local = false;
 				def->is_not_null = attribute->attnotnull;
@@ -2950,6 +2996,34 @@ MergeAttributes(List *schema, List *supers, char relpersistence,
 								 errdetail("%s versus %s", def->compression, newdef->compression)));
 				}
 
+				/*
+				 * Check encryption parameter.  All parents and children must
+				 * have the same encryption settings for a column.
+				 */
+				if ((def->encryption && !newdef->encryption) ||
+					(!def->encryption && newdef->encryption))
+				{
+					ereport(ERROR,
+							(errcode(ERRCODE_DATATYPE_MISMATCH),
+							 errmsg("column \"%s\" has an encryption specification conflict",
+									attributeName)));
+				}
+				else if (def->encryption && newdef->encryption)
+				{
+					FormData_pg_attribute a, newa;
+
+					GetColumnEncryption(NULL, def->encryption, &a);
+					GetColumnEncryption(NULL, newdef->encryption, &newa);
+
+					if (a.atttypid != newa.atttypid ||
+						a.atttypmod != newa.atttypmod ||
+						a.attcek != newa.attcek)
+						ereport(ERROR,
+								(errcode(ERRCODE_DATATYPE_MISMATCH),
+								 errmsg("column \"%s\" has an encryption specification conflict",
+										attributeName)));
+				}
+
 				/*
 				 * Merge of NOT NULL constraints = OR 'em together
 				 */
@@ -6897,6 +6971,19 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	attribute.attislocal = colDef->is_local;
 	attribute.attinhcount = colDef->inhcount;
 	attribute.attcollation = collOid;
+	if (colDef->encryption)
+	{
+		GetColumnEncryption(NULL, colDef->encryption, &attribute);
+		aclresult = object_aclcheck(ColumnEncKeyRelationId, attribute.attcek, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(attribute.attcek, false));
+	}
+	else
+	{
+		attribute.attcek = 0;
+		attribute.attusertypid = 0;
+		attribute.attusertypmod = -1;
+	}
 
 	ReleaseSysCache(typeTuple);
 
@@ -12728,6 +12815,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:
@@ -19328,3 +19418,130 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(ParseState *pstate, const List *coldefencryption, Form_pg_attribute attr)
+{
+	ListCell   *lc;
+	DefElem    *cekEl = NULL;
+	DefElem    *encdetEl = NULL;
+	DefElem    *algEl = NULL;
+	Oid			cekoid;
+	bool		encdet;
+	int			alg;
+
+	foreach(lc, coldefencryption)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "column_encryption_key") == 0)
+			defelp = &cekEl;
+		else if (strcmp(defel->defname, "encryption_type") == 0)
+			defelp = &encdetEl;
+		else if (strcmp(defel->defname, "algorithm") == 0)
+			defelp = &algEl;
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized column encryption parameter: %s", defel->defname));
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (cekEl)
+	{
+		List	   *val = defGetQualifiedName(cekEl);
+
+		cekoid = get_cek_oid(val, false);
+	}
+	else
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+				errmsg("column encryption key must be specified"));
+
+	if (encdetEl)
+	{
+		char	   *val = strVal(linitial(castNode(TypeName, encdetEl->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
+		encdet  = false;
+
+	if (algEl)
+	{
+		char	   *val = strVal(algEl->arg);
+
+		alg = get_cekalg_num(val);
+
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val));
+	}
+	else
+		alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+	attr->attcek = cekoid;
+	attr->attusertypid = attr->atttypid;
+	attr->attusertypmod = attr->atttypmod;
+
+	/* 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;
+
+	attr->atttypmod = alg;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(const FormData_pg_attribute *attr)
+{
+	List	   *result;
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	char	   *nspname;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attr->attcek));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attr->attcek);
+
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+	nspname = get_namespace_name(form->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(form->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->atttypmod))),
+									-1));
+
+	ReleaseSysCache(tup);
+
+	return result;
+}
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index f0f2e07655..c65f6a4388 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/commands/view.c b/src/backend/commands/view.c
index ff98c773f5..171dca3803 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -88,6 +88,26 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+				Form_pg_attribute orig_att;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				orig_att = (Form_pg_attribute) GETSTRUCT(tp);
+				def->typeName = makeTypeNameFromOid(orig_att->attusertypid,
+													orig_att->attusertypmod);
+				def->encryption = makeColumnEncryption(orig_att);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index dc8415a693..c81bc15128 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -3983,6 +3983,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/gram.y b/src/backend/parser/gram.y
index a0138382a1..a6039878fa 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -280,6 +280,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -419,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
@@ -592,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
@@ -690,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
@@ -708,13 +711,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 	JOIN
 
-	KEY
+	KEY KEYS
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
 	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
@@ -942,6 +945,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -1988,7 +1993,7 @@ CheckPointStmt:
 
 /*****************************************************************************
  *
- * DISCARD { ALL | TEMP | PLANS | SEQUENCES }
+ * DISCARD
  *
  *****************************************************************************/
 
@@ -2014,6 +2019,13 @@ DiscardStmt:
 					n->target = DISCARD_TEMP;
 					$$ = (Node *) n;
 				}
+			| DISCARD COLUMN ENCRYPTION KEYS
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+
+					n->target = DISCARD_COLUMN_ENCRYPTION_KEYS;
+					$$ = (Node *) n;
+				}
 			| DISCARD PLANS
 				{
 					DiscardStmt *n = makeNode(DiscardStmt);
@@ -3700,14 +3712,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;
@@ -3716,8 +3729,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;
@@ -3774,6 +3787,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4034,6 +4052,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6270,6 +6289,33 @@ 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
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (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; }
@@ -6289,6 +6335,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)); }
@@ -6800,6 +6850,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -7611,6 +7663,24 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| COLUMN ENCRYPTION KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CEK;
+					n->objs = $4;
+					$$ = n;
+				}
+			| COLUMN MASTER KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CMK;
+					n->objs = $4;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9140,6 +9210,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -9817,6 +9907,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10148,6 +10258,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11304,6 +11432,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_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 any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -16791,6 +16965,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ESCAPE
 			| EVENT
@@ -16840,6 +17015,7 @@ unreserved_keyword:
 			| INVOKER
 			| ISOLATION
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -16854,6 +17030,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -17337,6 +17514,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ESCAPE
@@ -17404,6 +17582,7 @@ bare_label_keyword:
 			| ISOLATION
 			| JOIN
 			| KEY
+			| KEYS
 			| LABEL
 			| LANGUAGE
 			| LARGE_P
@@ -17425,6 +17604,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 2240284f21..a8a1855aa0 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,159 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			Expr	   *expr = te->expr;
+
+			/*
+			 * If it's a RelabelType, look inside.  (For encrypted columns,
+			 * this would typically be a typmod adjustment.)
+			 */
+			if (IsA(expr, RelabelType))
+				expr = castNode(RelabelType, expr)->arg;
+
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(expr, Param))
+			{
+				Param	   *p = (Param *) expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(expr, Var))
+			{
+				Var		   *v = (Var *) expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell   *lc;
+
+					foreach(lc, srcrte->values_lists)
+					{
+						List	   *values_list = lfirst_node(List, lc);
+						Expr	   *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, RelabelType))
+							value = castNode(RelabelType, value)->arg;
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach(lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index f9218f48aa..8d902292cd 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1035,8 +1035,16 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		 */
 		def = makeNode(ColumnDef);
 		def->colname = pstrdup(attributeName);
-		def->typeName = makeTypeNameFromOid(attribute->atttypid,
-											attribute->atttypmod);
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			def->typeName = makeTypeNameFromOid(attribute->attusertypid,
+												attribute->attusertypmod);
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(attribute);
+		}
+		else
+			def->typeName = makeTypeNameFromOid(attribute->atttypid,
+												attribute->atttypmod);
 		def->inhcount = 0;
 		def->is_local = true;
 		def->is_not_null = attribute->attnotnull;
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 2552327d90..79ec9c4d1f 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2210,12 +2210,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 cab709b07b..b577557384 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -71,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"
 
@@ -1815,6 +1817,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 $%d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2560,6 +2572,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2618,11 +2632,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	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 = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				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 + 1)));
+
+			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->attusertypid;
+			pcekid = orig_att->attcek;
+			pcekalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x0001;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&row_description_buf, (int) pcekid);
+			pq_sendint32(&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 c7d9d96b45..180f86cb79 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,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1441,6 +1444,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 +1914,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2225,6 +2244,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 +2665,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 +2791,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;
@@ -2917,6 +2954,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_ALL:
 					tag = CMDTAG_DISCARD_ALL;
 					break;
+				case DISCARD_COLUMN_ENCRYPTION_KEYS:
+					tag = CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS;
+					break;
 				case DISCARD_PLANS:
 					tag = CMDTAG_DISCARD_PLANS;
 					break;
@@ -3063,6 +3103,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3688,6 +3736,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 8f7522d103..6aeb06f8fd 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -22,6 +22,8 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_foreign_data_wrapper.h"
 #include "catalog/pg_foreign_server.h"
@@ -101,6 +103,10 @@ static AclMode convert_table_priv_string(text *priv_type_text);
 static AclMode convert_sequence_priv_string(text *priv_type_text);
 static AttrNumber convert_column_name(Oid tableoid, text *column);
 static AclMode convert_column_priv_string(text *priv_type_text);
+static Oid	convert_column_encryption_key_name(text *cekname);
+static AclMode convert_column_encryption_key_priv_string(text *priv_type_text);
+static Oid	convert_column_master_key_name(text *cmkname);
+static AclMode convert_column_master_key_priv_string(text *priv_type_text);
 static Oid	convert_database_name(text *databasename);
 static AclMode convert_database_priv_string(text *priv_type_text);
 static Oid	convert_foreign_data_wrapper_name(text *fdwname);
@@ -800,6 +806,14 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			/* for backwards compatibility, grant some rights by default */
 			world_default = ACL_CREATE_TEMP | ACL_CONNECT;
@@ -911,6 +925,12 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 's':
 			objtype = OBJECT_SEQUENCE;
 			break;
+		case 'Y':
+			objtype = OBJECT_CEK;
+			break;
+		case 'y':
+			objtype = OBJECT_CMK;
+			break;
 		case 'd':
 			objtype = OBJECT_DATABASE;
 			break;
@@ -2915,6 +2935,384 @@ convert_column_priv_string(text *priv_type_text)
 }
 
 
+/*
+ * has_column_encryption_key_privilege variants
+ *		These are all named "has_column_encryption_key_privilege" at the SQL level.
+ *		They take various combinations of column encryption key name,
+ *		cek OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_encryption_key_privilege_name_name
+ *		Check user privileges on a column encryption key given
+ *		name username, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name
+ *		Check user privileges on a column encryption key given
+ *		text cekname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cekname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name_id
+ *		Check user privileges on a column encryption key given
+ *		name usename, column encryption key oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id
+ *		Check user privileges on a column encryption key given
+ *		column encryption key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cekid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_name
+ *		Check user privileges on a column encryption key given
+ *		roleid, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_id
+ *		Check user privileges on a column encryption key given
+ *		roleid, cek oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_encryption_key_privilege family.
+ */
+
+/*
+ * Given a CEK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_encryption_key_name(text *cekname)
+{
+	return get_cek_oid(textToQualifiedNameList(cekname), false);
+}
+
+/*
+ * convert_column_encryption_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_encryption_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_encryption_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_encryption_key_priv_map);
+}
+
+
+/*
+ * has_column_master_key_privilege variants
+ *		These are all named "has_column_master_key_privilege" at the SQL level.
+ *		They take various combinations of column master key name,
+ *		cmk OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_master_key_privilege_name_name
+ *		Check user privileges on a column master key given
+ *		name username, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name
+ *		Check user privileges on a column master key given
+ *		text cmkname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cmkname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name_id
+ *		Check user privileges on a column master key given
+ *		name usename, column master key oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id
+ *		Check user privileges on a column master key given
+ *		column master key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cmkid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_name
+ *		Check user privileges on a column master key given
+ *		roleid, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_id
+ *		Check user privileges on a column master key given
+ *		roleid, cmk oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_master_key_privilege family.
+ */
+
+/*
+ * Given a CMK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_master_key_name(text *cmkname)
+{
+	return get_cmk_oid(textToQualifiedNameList(cmkname), false);
+}
+
+/*
+ * convert_column_master_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_master_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_master_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_master_key_priv_map);
+}
+
+
 /*
  * has_database_privilege variants
  *		These are all named "has_database_privilege" at the SQL level.
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index 5778e3f0ef..dd21fcf59e 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -679,6 +679,113 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	Node	   *escontext = fcinfo->context;
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ;	/* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index c07382051d..7ad159110f 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
  *
@@ -3683,3 +3705,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+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;
+}
+
+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 77c2ba3f8f..d40af13efe 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -97,8 +97,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -551,7 +549,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 94abede512..eb432260da 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"
@@ -219,6 +222,32 @@ static const struct cachedesc cacheinfo[] = {
 			Anum_pg_cast_casttarget),
 		256
 	},
+	[CEKDATACEKCMK] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyCekidCmkidIndexId,
+		KEY(Anum_pg_colenckeydata_ckdcekid,
+			Anum_pg_colenckeydata_ckdcmkid),
+		8
+	},
+	[CEKDATAOID] = {
+		ColumnEncKeyDataRelationId,
+		ColumnEncKeyDataOidIndexId,
+		KEY(Anum_pg_colenckeydata_oid),
+		8
+	},
+	[CEKNAMENSP] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyNameNspIndexId,
+		KEY(Anum_pg_colenckey_cekname,
+			Anum_pg_colenckey_ceknamespace),
+		8
+	},
+	[CEKOID] = {
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		KEY(Anum_pg_colenckey_oid),
+		8
+	},
 	[CLAAMNAMENSP] = {
 		OperatorClassRelationId,
 		OpclassAmNameNspIndexId,
@@ -233,6 +262,19 @@ static const struct cachedesc cacheinfo[] = {
 		KEY(Anum_pg_opclass_oid),
 		8
 	},
+	[CMKNAMENSP] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyNameNspIndexId,
+		KEY(Anum_pg_colmasterkey_cmkname,
+			Anum_pg_colmasterkey_cmknamespace),
+		8
+	},
+	[CMKOID] = {
+		ColumnMasterKeyRelationId,
+		ColumnMasterKeyOidIndexId,
+		KEY(Anum_pg_colmasterkey_oid),
+		8
+	},
 	[COLLNAMEENCNSP] = {
 		CollationRelationId,
 		CollationNameEncNspIndexId,
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 033647011b..4b6be68f27 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"
@@ -130,6 +132,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())
 	{
 		/*
@@ -237,6 +245,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,
@@ -297,7 +311,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 a2b74901e4..f35df16080 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -201,6 +203,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);
 
@@ -876,6 +884,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 079693585c..eeed0db211 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -484,6 +484,10 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('U', "USAGE");
 	}
+	else if (strcmp(type, "COLUMN ENCRYPTION KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "COLUMN MASTER KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "DATABASE") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index aba780ef4b..afba79b2ea 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -85,6 +85,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 61ebb8fe85..bc303550fd 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3396,6 +3396,8 @@ _getObjectDescription(PQExpBuffer buf, const 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, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index f766b65059..c90c2803fc 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 4217908f84..f57bccee23 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "dumputils.h"
@@ -228,6 +229,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);
@@ -393,6 +396,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},
@@ -685,6 +689,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");
 
@@ -1056,6 +1063,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"
@@ -5587,6 +5595,164 @@ 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_ceknamespace;
+	int			i_cekowner;
+	int			i_cekacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, cek.cekowner, cek.cekacl, acldefault('Y', cek.cekowner) AS acldefault\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_ceknamespace = PQfnumber(res, "ceknamespace");
+	i_cekowner = PQfnumber(res, "cekowner");
+	i_cekacl = PQfnumber(res, "cekacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cekacl));
+		cekinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cekinfo[i].dacl.privtype = 0;
+		cekinfo[i].dacl.initprivs = NULL;
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, 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].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			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);
+		if (!PQgetisnull(res, i, i_cekacl))
+			cekinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	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_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+	int			i_cmkacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 160000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, cmk.cmkowner, cmk.cmkrealm, cmk.cmkacl, acldefault('y', cmk.cmkowner) AS acldefault\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_cmknamespace = PQfnumber(res, "cmknamespace");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+	i_cmkacl = PQfnumber(res, "cmkacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		cmkinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cmkacl));
+		cmkinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cmkinfo[i].dacl.privtype = 0;
+		cmkinfo[i].dacl.initprivs = NULL;
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cmkacl))
+			cmkinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8202,6 +8368,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8261,8 +8430,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
+	appendPQExpBuffer(q, "SELECT\n"
 						 "a.attrelid,\n"
 						 "a.attnum,\n"
 						 "a.attname,\n"
@@ -8275,7 +8443,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "a.attlen,\n"
 						 "a.attalign,\n"
 						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
+						 "pg_catalog.format_type(%s) AS atttypname,\n"
 						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
 						 "CASE WHEN a.attcollation <> t.typcollation "
 						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
@@ -8284,7 +8452,10 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 						 "' ' || pg_catalog.quote_literal(option_value) "
 						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
 						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+						 "), E',\n    ') AS attfdwoptions,\n",
+						 fout->remoteVersion >= 160000 ?
+						 "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END, CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END" :
+						 "a.atttypid, a.atttypmod");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -8310,10 +8481,23 @@ 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,
+						  "a.attcek,\n"
+						  "CASE a.atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "CASE WHEN a.atttypid IN (%u, %u) THEN a.atttypmod END AS attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID,
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcek,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
@@ -8338,6 +8522,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	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");
@@ -8398,6 +8585,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->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		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));
@@ -8425,6 +8615,22 @@ 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_attcek))
+			{
+				Oid		attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[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));
@@ -9943,6 +10149,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;
@@ -13357,6 +13569,141 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * 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",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_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,
+								  .namespace = cekinfo->dobj.namespace->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,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cekinfo->dobj.dumpId, InvalidDumpId, "COLUMN ENCRYPTION KEY",
+				qcekname, NULL, cekinfo->dobj.namespace->dobj.name,
+				cekinfo->rolname, &cekinfo->dacl);
+
+	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",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	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,
+								  .namespace = cmkinfo->dobj.namespace->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,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cmkinfo->dobj.dumpId, InvalidDumpId, "COLUMN MASTER KEY",
+				qcmkname, NULL, cmkinfo->dobj.namespace->dobj.name,
+				cmkinfo->rolname, &cmkinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -15442,6 +15789,22 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[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_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -18010,6 +18373,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 cdca0b993d..d4a2e595d0 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,
@@ -332,6 +334,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	struct _CekInfo **attcek;
+	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 */
@@ -663,6 +668,32 @@ typedef struct _SubscriptionInfo
 	char	   *subpublications;
 } SubscriptionInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	struct _CmkInfo **cekcmks;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	DumpableAcl	dacl;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -683,6 +714,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -710,6 +743,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 8266c117a3..d3dacd39da 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 cd421c5944..6530fb81a2 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 9c354213ce..ccbd27c23c 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -719,6 +719,18 @@
 		unlike    => { %dump_test_schema_runs, no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_owner => 1, },
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like   => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, 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, },
@@ -1319,6 +1331,26 @@
 		like      => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql   => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql   => 'DO $$
@@ -1737,6 +1769,26 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql   => "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp       => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql   => "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp       => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -3570,6 +3622,26 @@
 		unlike => { no_privs => 1, },
 	},
 
+	'GRANT USAGE ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
+	'GRANT USAGE ON COLUMN MASTER KEY cmk1' => {
+		create_order => 85,
+		create_sql   => 'GRANT USAGE ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;\E
+			/xm,
+		like => { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => { exclude_dump_test_schema => 1, no_privs => 1, },
+	},
+
 	'GRANT USAGE ON FOREIGN DATA WRAPPER dummy' => {
 		create_order => 85,
 		create_sql   => 'GRANT USAGE ON FOREIGN DATA WRAPPER dummy
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 955397ee9d..0d6a46d24b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -815,7 +815,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 99e28f607e..8f21fabd99 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1539,7 +1539,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;
@@ -1555,6 +1555,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;
@@ -1577,6 +1578,8 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attusertypid;
+	const char *attusertypmod;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1853,7 +1856,17 @@ 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)");
+	if (pset.sversion >= 160000)
+	{
+		attusertypid = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypid ELSE a.atttypid END";
+		attusertypmod = "CASE WHEN a.attusertypid <> 0 THEN a.attusertypmod ELSE a.atttypmod END";
+	}
+	else
+	{
+		attusertypid = "a.atttypid";
+		attusertypmod = "a.atttypmod";
+	}
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, %s)", attusertypid, attusertypmod);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1866,7 +1879,8 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		attrdef_col = cols++;
 		attnotnull_col = cols++;
-		appendPQExpBufferStr(&buf, ",\n  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
+		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");
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
@@ -1918,6 +1932,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 160000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			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 ||
@@ -2041,6 +2067,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)
@@ -2133,6 +2161,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),
@@ -4486,6 +4525,152 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cekacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 160000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cmkacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cmk.oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 554fe86725..1cf8f72176 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -76,6 +76,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e45c4aaca5..1729966959 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -252,6 +252,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -413,6 +415,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 73d4b393bc..010bc5a6d5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -137,6 +137,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 5a28b6f713..6736505c3a 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 8f12af799b..2f0ca71981 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -933,6 +933,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1223,6 +1237,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
@@ -1705,7 +1721,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1952,6 +1968,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_SCHEMA_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", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2894,6 +2926,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",
@@ -3605,7 +3657,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 	else if (Matches("DISCARD"))
-		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP");
+		COMPLETE_WITH("ALL", "COLUMN ENCRYPTION KEYS", "PLANS", "SEQUENCES", "TEMP");
 
 /* DO */
 	else if (Matches("DO"))
@@ -3619,6 +3671,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) ||
@@ -3931,6 +3984,8 @@ psql_completion(const char *text, int start, int end)
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"COLUMN ENCRYPTION KEY",
+											"COLUMN MASTER KEY",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4046,6 +4101,16 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+	/* Complete "GRANT/REVOKE * ON COLUMN ENCRYPTION|MASTER KEY *" with TO/FROM */
+	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
+			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny))
+	{
+		if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny, MatchAny))
+			COMPLETE_WITH("TO");
+		else
+			COMPLETE_WITH("FROM");
+	}
+
 	/* Complete "GRANT/REVOKE * ON FOREIGN DATA WRAPPER *" with TO/FROM */
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny))
diff --git a/src/common/Makefile b/src/common/Makefile
index 113029bf7b..73dce1150e 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	archive.o \
 	base64.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 0000000000..38ca72c898
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,107 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ *
+ * This only supports algorithms that have a mapping in JWA.  For any other
+ * ones, it returns NULL.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		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";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index 41bd58ebdf..3695d3285b 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -4,6 +4,7 @@ common_sources = files(
   'archive.c',
   'base64.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index 747ecb800d..4e384bbcdb 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,9 +20,13 @@ 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);
 
+extern void DiscardColumnEncryptionKeys(void);
+
 extern void debugStartup(DestReceiver *self, int operation,
 						 TupleDesc typeinfo);
 extern bool debugtup(TupleTableSlot *slot, DestReceiver *self);
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index ffd5e9dc82..eb59e73c0a 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/heap.h b/src/include/catalog/heap.h
index d01ab504b6..758696b539 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 3179be09d3..9e2c5256f7 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -65,6 +65,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/namespace.h b/src/include/catalog/namespace.h
index f64a0ec26b..d0b9e8458d 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -115,6 +115,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index c4d6adcd3e..c58b79e3a7 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 5b950129de..0e9e85ebf3 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 b561e17781..7910175a6a 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -164,6 +164,17 @@ 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);
+
+	/*
+	 * User-visible type and typmod, currently used for encrypted columns.
+	 * These are only set to nondefault values if they are different from
+	 * atttypid and attypmod.
+	 */
+	Oid			attusertypid BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_type);
+	int32		attusertypmod BKI_DEFAULT(-1);
+
 	/* Number of times inherited from direct parent relation(s) */
 	int32		attinhcount BKI_DEFAULT(0);
 
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 0000000000..c57fa18a27
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,46 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		cekacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_TOAST(pg_colenckey, 8263, 8264);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, on pg_colenckey using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_nsp_index, 8242, ColumnEncKeyNameNspIndexId, on pg_colenckey using btree(cekname name_ops, ceknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 0000000000..c88e7e65ad
--- /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-2023, 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);
+	int32		ckdcmkalg;		/* PG_CMK_* values */
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	bytea		ckdencval BKI_FORCE_NOT_NULL;
+#endif
+} FormData_pg_colenckeydata;
+
+typedef FormData_pg_colenckeydata *Form_pg_colenckeydata;
+
+DECLARE_TOAST(pg_colenckeydata, 8237, 8238);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckeydata_oid_index, 8251, ColumnEncKeyDataOidIndexId, on pg_colenckeydata using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, on pg_colenckeydata using btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 0000000000..d3bfd36279
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,47 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2023, 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			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+	aclitem		cmkacl[1] BKI_DEFAULT(_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_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, on pg_colmasterkey using btree(cmkname name_ops, cmknamespace oid_ops));
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index c867d99563..ff06d52fd0 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 b2cdea66c4..114279fa64 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 91587b99d0..c21052a3f7 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 505595620e..b410388a42 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6355,6 +6355,14 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid',
+  prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -7142,6 +7150,68 @@
   proname => 'fmgr_sql_validator', provolatile => 's', prorettype => 'void',
   proargtypes => 'oid', prosrc => 'fmgr_sql_validator' },
 
+{ oid => '8265',
+  descr => 'user privilege on column encryption key by username, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_encryption_key_privilege_name_name' },
+{ oid => '8266',
+  descr => 'user privilege on column encryption key by username, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_encryption_key_privilege_name_id' },
+{ oid => '8267',
+  descr => 'user privilege on column encryption key by user oid, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_encryption_key_privilege_id_name' },
+{ oid => '8268',
+  descr => 'user privilege on column encryption key by user oid, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_encryption_key_privilege_id_id' },
+{ oid => '8269',
+  descr => 'current user privilege on column encryption key by column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_encryption_key_privilege_name' },
+{ oid => '8270',
+  descr => 'current user privilege on column encryption key by column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_encryption_key_privilege_id' },
+
+{ oid => '8271',
+  descr => 'user privilege on column master key by username, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_master_key_privilege_name_name' },
+{ oid => '8272',
+  descr => 'user privilege on column master key by username, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_master_key_privilege_name_id' },
+{ oid => '8273',
+  descr => 'user privilege on column master key by user oid, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_master_key_privilege_id_name' },
+{ oid => '8274',
+  descr => 'user privilege on column master key by user oid, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_master_key_privilege_id_id' },
+{ oid => '8275',
+  descr => 'current user privilege on column master key by column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_master_key_privilege_name' },
+{ oid => '8276',
+  descr => 'current user privilege on column master key by column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_master_key_privilege_id' },
+
 { oid => '2250',
   descr => 'user privilege on database by username, database name',
   proname => 'has_database_privilege', provolatile => 's', prorettype => 'bool',
@@ -11939,4 +12009,37 @@
   proname => 'any_value_transfn', prorettype => 'anyelement',
   proargtypes => 'anyelement anyelement', prosrc => 'any_value_transfn' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_det',
+  prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd', proargtypes => 'cstring',
+  prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd', proargtypes => 'internal',
+  prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea', proargtypes => 'pg_encrypted_rnd',
+  prosrc => 'pg_encrypted_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 92bcaf2c73..b9d3920a97 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -692,4 +692,17 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+# Note: typstorage 'e' since compression is not useful for encrypted data
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_det_in', typoutput => 'pg_encrypted_det_out',
+  typreceive => 'pg_encrypted_det_recv', typsend => 'pg_encrypted_det_send', typalign => 'i',
+  typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f', typtype => 'b',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in', typoutput => 'pg_encrypted_rnd_out',
+  typreceive => 'pg_encrypted_rnd_recv', typsend => 'pg_encrypted_rnd_send', typalign => 'i',
+  typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index 519e570c8c..3c7ab2a8fe 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 0000000000..7127e0ca5e
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2023, 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);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index e7c2b91a58..11f88c59ce 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -105,4 +105,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(const FormData_pg_attribute *attr);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 0000000000..212587d222
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml>.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index ac6407e9f6..39a286e58a 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 371aa0ffc5..4253d531f8 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -721,6 +721,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? */
@@ -757,11 +758,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -1979,6 +1981,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2166,6 +2171,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
@@ -3644,6 +3674,7 @@ typedef struct CheckPointStmt
 typedef enum DiscardMode
 {
 	DISCARD_ALL,
+	DISCARD_COLUMN_ENCRYPTION_KEYS,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
 	DISCARD_TEMP
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index bb36213e6f..c6da932042 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)
@@ -228,6 +229,7 @@ PG_KEYWORD("isnull", ISNULL, TYPE_FUNC_NAME_KEYWORD, AS_LABEL)
 PG_KEYWORD("isolation", ISOLATION, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("join", JOIN, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("keys", KEYS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("label", LABEL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("language", LANGUAGE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("large", LARGE_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -250,6 +252,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_param.h b/src/include/parser/parse_param.h
index d4865e50f6..26e1cd617a 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index e738ac1c09..a5e61c9f5b 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)
@@ -130,6 +134,7 @@ PG_CMDTAG(CMDTAG_DECLARE_CURSOR, "DECLARE CURSOR", false, false, false)
 PG_CMDTAG(CMDTAG_DELETE, "DELETE", false, false, true)
 PG_CMDTAG(CMDTAG_DISCARD, "DISCARD", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
+PG_CMDTAG(CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS, "DISCARD COLUMN ENCRYPTION KEYS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false)
@@ -138,6 +143,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/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2..0c73022833 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -159,6 +159,8 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
+#define ACL_ALL_RIGHTS_CEK			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_CMK			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 4f5418b972..1dac24e4b4 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,9 @@ 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 char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, 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 a443181d41..8ecacb29df 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -207,6 +207,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h
index d5d50ceab4..bf1d527c1a 100644
--- a/src/include/utils/syscache.h
+++ b/src/include/utils/syscache.h
@@ -44,8 +44,14 @@ enum SysCacheIdentifier
 	AUTHNAME,
 	AUTHOID,
 	CASTSOURCETARGET,
+	CEKDATACEKCMK,
+	CEKDATAOID,
+	CEKNAMENSP,
+	CEKOID,
 	CLAAMNAMENSP,
 	CLAOID,
+	CMKNAMENSP,
+	CMKOID,
 	COLLNAMEENCNSP,
 	COLLOID,
 	CONDEFAULT,
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index c18e914228..10dfda8016 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -54,6 +54,7 @@ endif
 
 ifeq ($(with_ssl),openssl)
 OBJS += \
+	fe-encrypt-openssl.o \
 	fe-secure-openssl.o
 endif
 
diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt
index e8bcc88370..8897aa243c 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
+PQexecPreparedDescribed   187
+PQsendQueryPreparedDescribed 188
+PQfisencrypted            189
+PQparamisencrypted        190
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 5638b223cb..f0abcfc37a 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}
@@ -1412,6 +1420,28 @@ connectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4061,6 +4091,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 0000000000..9baa47da13
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,839 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2023, 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 "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+#define pqResultAlloc(res, nBytes, isBinary) malloc(nBytes)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = 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;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								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);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05>
+ * for encrypting and decrypting column values.
+ */
+
+#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,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+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;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+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;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+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,
+					 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;
+	int64		al;
+	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;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	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;
+}
+
+/*
+ * Decrypt a column value
+ */
+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			iv_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 = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_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;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_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;
+	buf = pqResultAlloc(res, bufsize, false);
+	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;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#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)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "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 */
+
+/*
+ * Encrypt a column value
+ */
+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			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_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)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	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, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;	/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								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,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, 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, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, P, sizeof(P));
+
+	return 0;
+}
+
+#endif							/* TEST_ENCRYPT */
diff --git a/src/interfaces/libpq/fe-encrypt.h b/src/interfaces/libpq/fe-encrypt.h
new file mode 100644
index 0000000000..0b65f913da
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2023, 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, const char *cmkfilename, 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 ec62550e38..4bd9e4e309 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -72,7 +74,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1183,6 +1186,420 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+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 *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					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, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	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;
+
+	if (!conn->cmklookup || !conn->cmklookup[0])
+	{
+		libpq_append_conn_error(conn, "column master key lookup is not configured");
+		return NULL;
+	}
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', 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)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, PG_BINARY_R);
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+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
@@ -1251,13 +1668,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					goto fail;
+				}
+
+				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("protocol error: column encryption key associated with encrypted column was not sent by the server");
+					goto fail;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				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;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* 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;
@@ -1500,6 +1955,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1516,6 +1973,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1524,7 +2012,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1639,6 +2128,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1664,7 +2171,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2270,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2319,47 @@ PQsendQueryGuts(PGconn *conn,
 		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;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2378,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2396,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#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)
+				{
+					libpq_append_conn_error(conn, "protocol error: column encryption key associated with encrypted parameter was not sent by the server");
+					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
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
+				goto sendFailed;
+#endif
+			}
+			else
+			{
 			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+				pqPutnchar(paramValue, nbytes, conn) < 0)
 				goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2290,12 +2878,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3539,7 +4146,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3577,6 +4194,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)
 {
@@ -3762,6 +4390,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 8ab6a88416..617e641cf6 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);
@@ -297,6 +299,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case 'y':		/* Column Master Key */
+					getColumnMasterKey(conn);
+					break;
+				case 'Y':		/* Column Encryption Key */
+					getColumnEncryptionKey(conn);
+					break;
 				case 'T':		/* Row Description */
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -358,8 +366,21 @@ pqParseInput3(PGconn *conn)
 					}
 					break;
 				case 't':		/* Parameter Description */
-					if (getParamDescriptions(conn, msgLength))
-						return;
+					if (conn->error_result ||
+						(conn->result != NULL &&
+						 conn->result->resultStatus == PGRES_FATAL_ERROR))
+					{
+						/*
+						 * We've already choked for some reason.  Just discard
+						 * the data till we get to the end of the query.
+						 */
+						conn->inCursor += msgLength;
+					}
+					else
+					{
+						if (getParamDescriptions(conn, msgLength))
+							return;
+					}
 					break;
 				case 'D':		/* Data Row */
 					if (conn->result != NULL &&
@@ -547,6 +568,9 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		int			typlen;
 		int			atttypmod;
 		int			format;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGets(&conn->workBuffer, conn) ||
 			pqGetInt(&tableid, 4, conn) ||
@@ -561,6 +585,23 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, conn) ||
+				pqGetInt(&flags, 2, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -582,6 +623,8 @@ 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)
 			result->binary = 0;
@@ -685,10 +728,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, 4, 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! */
@@ -1468,6 +1532,92 @@ 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);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	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;
+	int			ret;
+
+	/* 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, 4, 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 */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2286,6 +2436,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 abaab6a073..5c2793a5eb 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);
+			pqTraceOutputInt32(f, message, cursor, false);
+			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,12 @@ 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);
+			pqTraceOutputInt32(f, message, cursor, false);
+			pqTraceOutputInt16(f, message, cursor);
+		}
 	}
 }
 
@@ -514,6 +530,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);
+	pqTraceOutputInt32(f, message, cursor, false);
+	len = pqTraceOutputInt32(f, message, cursor, false);
+	pqTraceOutputNchar(f, len, message, cursor);
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -647,10 +687,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 +707,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 f3d9220496..cf339a3e53 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,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -461,6 +471,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern PGresult *PQgetResult(PGconn *conn);
 
@@ -531,6 +549,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 +559,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 d7ec5ed429..05b5891ffe 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 */
 
 	/* 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);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index 3cd0ddb494..19ab52c112 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -29,6 +29,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
 
@@ -116,6 +117,7 @@ tests += {
     'tests': [
       't/001_uri.pl',
       't/002_api.pl',
+      't/003_encrypt.pl',
     ],
     'env': {'with_ssl': ssl_library},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index b6ed455183..9aef1c7a29 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -3,6 +3,7 @@ CATALOG_NAME     = libpq
 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 \
diff --git a/src/interfaces/libpq/t/003_encrypt.pl b/src/interfaces/libpq/t/003_encrypt.pl
new file mode 100644
index 0000000000..94a7441037
--- /dev/null
+++ b/src/interfaces/libpq/t/003_encrypt.pl
@@ -0,0 +1,70 @@
+# Copyright (c) 2023, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build' if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like([ 'libpq_test_encrypt' ],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb67..1846594ec5 100644
--- a/src/interfaces/libpq/test/.gitignore
+++ b/src/interfaces/libpq/test/.gitignore
@@ -1,2 +1,3 @@
+/libpq_test_encrypt
 /libpq_testclient
 /libpq_uri_regress
diff --git a/src/interfaces/libpq/test/Makefile b/src/interfaces/libpq/test/Makefile
index 75ac08f943..b1ebab90d4 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 b2a4b06fd2..87d2808b52 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874..c9a3868053 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),column_encryption examples kerberos icu ldap ssl)
 
 # We want to recurse to all subdirs for all standard targets, except that
 # installcheck and install should not recurse into the subdirectory "modules".
diff --git a/src/test/column_encryption/.gitignore b/src/test/column_encryption/.gitignore
new file mode 100644
index 0000000000..456dbf69d2
--- /dev/null
+++ b/src/test/column_encryption/.gitignore
@@ -0,0 +1,3 @@
+/test_client
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/column_encryption/Makefile b/src/test/column_encryption/Makefile
new file mode 100644
index 0000000000..764cadf550
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2023, 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
+
+export OPENSSL
+
+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 0000000000..84cfa84e12
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,23 @@
+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',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+    },
+  },
+}
diff --git a/src/test/column_encryption/t/001_column_encryption.pl b/src/test/column_encryption/t/001_column_encryption.pl
new file mode 100644
index 0000000000..b68f4486a2
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,271 @@
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+my $perlbin = $^X;
+$perlbin =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out', "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$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"
+	);
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	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}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');});
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# 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\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[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 %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} = qq{*=run:"$perlbin" ./test_run_decrypt.pl %k %a %p};
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1}, stdout => \$stdout);
+	is($stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$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\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Test copy and restore
+
+my $copy_out = $node->safe_psql('postgres', q{COPY tbl1 TO STDOUT;});
+$node->safe_psql('postgres', q{CREATE TABLE tbl1_copy (LIKE tbl1 INCLUDING ENCRYPTED)});
+$node->safe_psql('postgres', q{COPY tbl1_copy FROM STDIN;} . "\n" . $copy_out . "\\\.\n");
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1_copy});
+is($result,
+	q(1|val1|11
+2|val2|22
+3|val3|33),
+	'decrypted query result after COPY dump and restore');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(['test_client', 'test3'],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format');
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(['test_client', 'test4'],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text');
+
+
+# Test UPDATE
+
+$node->safe_psql('postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$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 update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres', q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is($result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$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', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is($result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+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');
+
+is($node->safe_psql('postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$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 tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result,
+	q(1|valB1|valC1),
+	'decrypted query result multiple keys');
+
+$node->safe_psql('postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$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 second insert');
+
+
+done_testing();
diff --git a/src/test/column_encryption/t/002_cmk_rotation.pl b/src/test/column_encryption/t/002_cmk_rotation.pl
new file mode 100644
index 0000000000..14eafb8ec9
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,112 @@
+# Copyright (c) 2021-2023, 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 $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# 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, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');});
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+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, algorithm = '$cmkalg', 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 0000000000..9c257a3ddb
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2023, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	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;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	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 = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	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
+		ret = 88;
+
+	PQfinish(conn);
+	return ret;
+}
diff --git a/src/test/column_encryption/test_run_decrypt.pl b/src/test/column_encryption/test_run_decrypt.pl
new file mode 100755
index 0000000000..66871cb438
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,58 @@
+#!/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-2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl', '-decrypt',
+	'-inkey', "${tmpdir}/${cmkname}.pem", '-pkeyopt', 'rsa_padding_mode:oaep',
+	'-in', $filename, '-out', "${tmpdir}/output.tmp"
+);
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $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}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 5f3c9c2ba2..d55ddb1ab7 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -9,6 +9,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 0000000000..8a8a08533f
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,539 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE ROLE regress_enc_user1;
+CREATE COLUMN MASTER KEY fail WITH (
+    foo = bar
+);
+ERROR:  column master key attribute "foo" not recognized
+LINE 2:     foo = bar
+            ^
+CREATE COLUMN MASTER KEY fail WITH (
+    realm = 'test',
+    realm = 'test'
+);
+ERROR:  conflicting or redundant options
+LINE 3:     realm = 'test'
+            ^
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+-- duplicate
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+ERROR:  column master key "cmk1" already exists
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+CREATE COLUMN MASTER KEY cmk2;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+-- fail
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2', realm = 'test2');
+ERROR:  conflicting or redundant options
+LINE 1: ALTER COLUMN MASTER KEY cmk2a (realm = 'test2', realm = 'tes...
+                                                        ^
+-- fail
+ALTER COLUMN MASTER KEY cmk2a (foo = bar);
+ERROR:  column master key attribute "foo" not recognized
+LINE 1: ALTER COLUMN MASTER KEY cmk2a (foo = bar);
+                                       ^
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    foo = bar
+);
+ERROR:  column encryption key attribute "foo" not recognized
+LINE 2:     foo = bar
+            ^
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    column_master_key = cmk1
+);
+ERROR:  conflicting or redundant options
+LINE 3:     column_master_key = cmk1
+            ^
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  attribute "column_master_key" must be specified
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  attribute "algorithm" must be specified
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1'
+);
+ERROR:  attribute "encrypted_value" must be specified
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+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';
+-- duplicate
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "cek1" already exists
+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 (foo = bar)
+);
+ERROR:  unrecognized column encryption parameter: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (encryption_type = randomized)
+);
+ERROR:  column encryption key must be specified
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, column_encryption_key = cek1)
+);
+ERROR:  conflicting or redundant options
+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, encryption_type = deterministic);
+\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       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  permission denied for column master key cmk1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+ERROR:  permission denied for column encryption key cek1
+CREATE TABLE tbl_7040 (a int);
+-- fail
+ALTER TABLE tbl_7040 ADD COLUMN b text ENCRYPTED WITH (column_encryption_key = cek1);
+ERROR:  permission denied for column encryption key cek1
+DROP TABLE tbl_7040;
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column 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 in schema "public"
+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 in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION '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 SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+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
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, encrypted_value = '\xDEADBEEF');  -- fail
+ERROR:  attribute "encrypted_value" must not be specified
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+ERROR:  column encryption key "fail" does not exist
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf..8dbb4a847a 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -101,6 +103,7 @@ BEGIN
         ('materialized view'), ('foreign table'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
+        ('column encryption key'), ('column encryption key data'), ('column master key'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
         ('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
         ('text search parser'), ('text search dictionary'),
@@ -201,6 +204,24 @@ WARNING:  error for cast,{addr_nsp,zwei},{}: name list length must be exactly 1
 WARNING:  error for cast,{addr_nsp,zwei},{integer}: name list length must be exactly 1
 WARNING:  error for cast,{eins,zwei,drei},{}: name list length must be exactly 1
 WARNING:  error for cast,{eins,zwei,drei},{integer}: name list length must be exactly 1
+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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+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},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 WARNING:  error for table constraint,{eins},{}: must specify relation and object name
 WARNING:  error for table constraint,{eins},{integer}: must specify relation and object name
 WARNING:  error for table constraint,{addr_nsp,zwei},{}: relation "addr_nsp" does not exist
@@ -409,6 +430,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}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,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
@@ -547,6 +576,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
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|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 215eb899be..226f5e404e 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 {attusertypid} => 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,9 @@ 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 {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {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 02f5348ab1..d493ac5f7c 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ 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)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,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
@@ -872,6 +876,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
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index a640cfc476..742272225d 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,
@@ -716,6 +718,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 15e015b3d6..577a6d4b2e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 427429975e..57c7d2bec3 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -82,7 +82,7 @@ psql_start_test(const char *testname,
 					   bindir ? bindir : "",
 					   bindir ? "/" : "",
 					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
+					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on -v HIDE_COLUMN_ENCRYPTION=on",
 					   infile,
 					   outfile);
 	if (offset >= sizeof(psql_cmd))
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 0000000000..0c5f2af2da
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,371 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE ROLE regress_enc_user1;
+
+CREATE COLUMN MASTER KEY fail WITH (
+    foo = bar
+);
+
+CREATE COLUMN MASTER KEY fail WITH (
+    realm = 'test',
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+
+-- duplicate
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk2;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+-- fail
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2', realm = 'test2');
+
+-- fail
+ALTER COLUMN MASTER KEY cmk2a (foo = bar);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    foo = bar
+);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    column_master_key = cmk1
+);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1'
+);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+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';
+
+-- duplicate
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+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 (foo = bar)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (encryption_type = randomized)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, column_encryption_key = cek1)
+);
+
+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, encryption_type = deterministic);
+
+\d tbl_447f
+\d+ tbl_447f
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+CREATE TABLE tbl_7040 (a int);
+-- fail
+ALTER TABLE tbl_7040 ADD COLUMN b text ENCRYPTED WITH (column_encryption_key = cek1);
+DROP TABLE tbl_7040;
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+
+
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+
+
+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 SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+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
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, encrypted_value = '\xDEADBEEF');  -- fail
+
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d..61828613d9 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -93,6 +95,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
         ('materialized view'), ('foreign table'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
+        ('column encryption key'), ('column encryption key data'), ('column master key'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
         ('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
         ('text search parser'), ('text search dictionary'),
@@ -174,6 +177,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}', '{}'),
@@ -228,6 +234,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;
 
@@ -247,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
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index 79ec410a6c..2ba4c9a545 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -544,6 +544,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',

base-commit: 7b14e20b12cc8358cad9bdd05dd6b7de7f73c431
-- 
2.39.2

#75Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andres Freund (#73)
Re: Transparent column encryption

On 12.03.23 01:11, Andres Freund wrote:

Have you done benchmarks of some simple workloads to verify this doesn't cause
slowdowns (when not using encryption, obviously)? printtup.c is a performance
sensitive portion for simple queries, particularly when they return multiple
columns.

The additional code isn't used when column encryption is off, so there
shouldn't be any impact.

#76Andres Freund
andres@anarazel.de
In reply to: Peter Eisentraut (#75)
Re: Transparent column encryption

Hi,

On 2023-03-13 21:22:29 +0100, Peter Eisentraut wrote:

On 12.03.23 01:11, Andres Freund wrote:

Have you done benchmarks of some simple workloads to verify this doesn't cause
slowdowns (when not using encryption, obviously)? printtup.c is a performance
sensitive portion for simple queries, particularly when they return multiple
columns.

The additional code isn't used when column encryption is off, so there
shouldn't be any impact.

It adds branches, and it makes tupledescs wider. In tight spots, such as
printtup, that can hurt, even if the branches aren't ever entered.

Greetings,

Andres Freund

#77Andres Freund
andres@anarazel.de
In reply to: Andres Freund (#76)
Re: Transparent column encryption

Hi,

On 2023-03-13 13:41:19 -0700, Andres Freund wrote:

On 2023-03-13 21:22:29 +0100, Peter Eisentraut wrote:

On 12.03.23 01:11, Andres Freund wrote:

Have you done benchmarks of some simple workloads to verify this doesn't cause
slowdowns (when not using encryption, obviously)? printtup.c is a performance
sensitive portion for simple queries, particularly when they return multiple
columns.

The additional code isn't used when column encryption is off, so there
shouldn't be any impact.

It adds branches, and it makes tupledescs wider. In tight spots, such as
printtup, that can hurt, even if the branches aren't ever entered.

In fact, I do see a noticable, but not huge, regression:

$ cat /tmp/test.sql
SELECT * FROM pg_class WHERE oid = 1247;

c=1;taskset -c 10 pgbench -n -M prepared -c$c -j$c -f /tmp/test.sql -P1 -T10

with the server also pinned to core 1, and turbo boost disabled. Nothing else
is allowed to run on the core, or its hyperthread sibling. This is my setup
for comparing performance with the least noise in general, not related to this
patch.

head: 28495.858509 28823.055643 28731.074311
patch: 28298.498851 28285.426532 28489.359569

A ~1.1% loss.

pipelined 50 statements (pgbench pinned to a different otherwise unused core)
head: 1147.404506 1147.587475 1151.976547
patch: 1126.525708 1122.375337 1119.088734

A ~2.2% loss.

That might not be prohibitive, but it does seem worth analyzing.

Greetings,

Andres Freund

#78Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andres Freund (#77)
Re: Transparent column encryption

On 13.03.23 22:11, Andres Freund wrote:

It adds branches, and it makes tupledescs wider. In tight spots, such as
printtup, that can hurt, even if the branches aren't ever entered.

In fact, I do see a noticable, but not huge, regression:

I tried to reproduce your measurements, but I don't have the CPU
affinity tools like that on macOS, so the differences were lost in the
noise. (The absolute numbers looked very similar to yours.)

I can reproduce a regression if I keep adding more columns to
pg_attribute, like 8 OID columns does start to show.

I investigated whether I could move some columns to the
"variable-length" part outside the tuple descriptor, but that would
require major surgery in heap.c and tablecmds.c (MergeAttributes() ...
shudder), and also elsewhere, where you currently only have a tuple
descriptor for looking up stuff.

How do we proceed here? A lot of user-facing table management stuff
like compression, statistics, collation, dropped columns, and probably
future features like column reordering or periods, have to be
represented in pg_attribute somehow, at least in the current
architecture. Do we hope that hardware keeps up with the pace at which
we add new features? Do we need to decouple tuple descriptors from
pg_attribute altogether? How do we decide what goes into the tuple
descriptor and what does not? I'm interested in addressing this,
because obviously we do want the ability to add more features in the
future, but I don't know what the direction should be.

#79Andres Freund
andres@anarazel.de
In reply to: Peter Eisentraut (#78)
Re: Transparent column encryption

Hi,

On 2023-03-16 11:26:46 +0100, Peter Eisentraut wrote:

On 13.03.23 22:11, Andres Freund wrote:

It adds branches, and it makes tupledescs wider. In tight spots, such as
printtup, that can hurt, even if the branches aren't ever entered.

In fact, I do see a noticable, but not huge, regression:

I tried to reproduce your measurements, but I don't have the CPU affinity
tools like that on macOS, so the differences were lost in the noise. (The
absolute numbers looked very similar to yours.)

I can reproduce a regression if I keep adding more columns to pg_attribute,
like 8 OID columns does start to show.

[...]
How do we proceed here?

Maybe a daft question, but why do we need a separate type and typmod for
encrypted columns? Why isn't the fact that the column is encrypted exactly one
new field, and we use the existing type/typmod fields?

A lot of user-facing table management stuff like compression, statistics,
collation, dropped columns, and probably future features like column
reordering or periods, have to be represented in pg_attribute somehow, at
least in the current architecture. Do we hope that hardware keeps up with
the pace at which we add new features?

Yea, it's not great as is. I think it's also OK to decide that the slowdown is
worth it in some cases - it just has to be a conscious decision, in the open.

Do we need to decouple tuple descriptors from pg_attribute altogether?

Yes. Very clearly. The amount of memory and runtime we spent on tupledescs is
disproportionate. A second angle is that we build tupledescs way way too
frequently. The executor infers them everywhere, so not even prepared
statements protect against that.

How do we decide what goes into the tuple descriptor and what does not? I'm
interested in addressing this, because obviously we do want the ability to
add more features in the future, but I don't know what the direction should
be.

We've had some prior discussion around this, see e.g.
/messages/by-id/20210819114435.6r532qbadcsyfscp@alap3.anarazel.de

Greetings,

Andres Freund

#80Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andres Freund (#79)
Re: Transparent column encryption

On 16.03.23 17:36, Andres Freund wrote:

Maybe a daft question, but why do we need a separate type and typmod for
encrypted columns? Why isn't the fact that the column is encrypted exactly one
new field, and we use the existing type/typmod fields?

The way this is implemented is that for an encrypted column, the real
atttypid and atttypmod are one of the encrypted special types
(pg_encrypted_*). That way, most of the system doesn't need to care
about the details of encryption or whatnot, it just unpacks tuples etc.
by looking at atttypid, atttyplen, etc., and queries on encrypted data
behave normally by just looking at what operators etc. those types have.
This approach heavily contains the number of places that need to know
about this feature at all.

Do we need to decouple tuple descriptors from pg_attribute altogether?

Yes. Very clearly. The amount of memory and runtime we spent on tupledescs is
disproportionate. A second angle is that we build tupledescs way way too
frequently. The executor infers them everywhere, so not even prepared
statements protect against that.

How do we decide what goes into the tuple descriptor and what does not? I'm
interested in addressing this, because obviously we do want the ability to
add more features in the future, but I don't know what the direction should
be.

We've had some prior discussion around this, see e.g.
/messages/by-id/20210819114435.6r532qbadcsyfscp@alap3.anarazel.de

This sounds like a good plan.

#81Andres Freund
andres@anarazel.de
In reply to: Peter Eisentraut (#80)
Re: Transparent column encryption

Hi,

On 2023-03-21 18:05:15 +0100, Peter Eisentraut wrote:

On 16.03.23 17:36, Andres Freund wrote:

Maybe a daft question, but why do we need a separate type and typmod for
encrypted columns? Why isn't the fact that the column is encrypted exactly one
new field, and we use the existing type/typmod fields?

The way this is implemented is that for an encrypted column, the real
atttypid and atttypmod are one of the encrypted special types
(pg_encrypted_*). That way, most of the system doesn't need to care about
the details of encryption or whatnot, it just unpacks tuples etc. by looking
at atttypid, atttyplen, etc., and queries on encrypted data behave normally
by just looking at what operators etc. those types have. This approach
heavily contains the number of places that need to know about this feature
at all.

I get that for the type, but why do we need the typmod duplicated as well?

Greetings,

Andres Freund

#82Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andres Freund (#81)
Re: Transparent column encryption

On 21.03.23 18:47, Andres Freund wrote:

On 2023-03-21 18:05:15 +0100, Peter Eisentraut wrote:

On 16.03.23 17:36, Andres Freund wrote:

Maybe a daft question, but why do we need a separate type and typmod for
encrypted columns? Why isn't the fact that the column is encrypted exactly one
new field, and we use the existing type/typmod fields?

The way this is implemented is that for an encrypted column, the real
atttypid and atttypmod are one of the encrypted special types
(pg_encrypted_*). That way, most of the system doesn't need to care about
the details of encryption or whatnot, it just unpacks tuples etc. by looking
at atttypid, atttyplen, etc., and queries on encrypted data behave normally
by just looking at what operators etc. those types have. This approach
heavily contains the number of places that need to know about this feature
at all.

I get that for the type, but why do we need the typmod duplicated as well?

Earlier patch versions didn't do that, but that got really confusing
about which type the typmod really belonged to, since code currently
assumes that typid+typmod makes sense. Earlier patch versions had three
fields (usertypid, keyid, encalg), and then I changed it to (usertypid,
usertypmod, keyid) and instead placed the encalg into the real typmod,
which made everything much cleaner.

#83Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Peter Eisentraut (#82)
Re: Transparent column encryption

On 22.03.23 10:00, Peter Eisentraut wrote:

I get that for the type, but why do we need the typmod duplicated as
well?

Earlier patch versions didn't do that, but that got really confusing
about which type the typmod really belonged to, since code currently
assumes that typid+typmod makes sense.  Earlier patch versions had three
fields (usertypid, keyid, encalg), and then I changed it to (usertypid,
usertypmod, keyid) and instead placed the encalg into the real typmod,
which made everything much cleaner.

I thought about this some more. I think we could get rid of
attusertypmod and just hardcode it as -1. The idea would be that if you
ask for an encrypted column of type, say, varchar(500), the server isn't
able to enforce that anyway, so we could just prohibit specifying a
nondefault typmod for encrypted columns.

I'm not sure if there are weird types that use typmods in some way where
this wouldn't work. But so far I could not think of anything.

I'll look into this some more.

#84Robert Haas
robertmhaas@gmail.com
In reply to: Peter Eisentraut (#83)
Re: Transparent column encryption

On Thu, Mar 23, 2023 at 9:55 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

I thought about this some more. I think we could get rid of
attusertypmod and just hardcode it as -1. The idea would be that if you
ask for an encrypted column of type, say, varchar(500), the server isn't
able to enforce that anyway, so we could just prohibit specifying a
nondefault typmod for encrypted columns.

I'm not sure if there are weird types that use typmods in some way where
this wouldn't work. But so far I could not think of anything.

I'll look into this some more.

I thought we often treated atttypid, atttypmod, and attcollation as a
trio, these days. It seems a bit surprising that you'd end up adding
columns for two out of the three.

--
Robert Haas
EDB: http://www.enterprisedb.com

#85Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Robert Haas (#84)
Re: Transparent column encryption

On 23.03.23 16:55, Robert Haas wrote:

On Thu, Mar 23, 2023 at 9:55 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:

I thought about this some more. I think we could get rid of
attusertypmod and just hardcode it as -1. The idea would be that if you
ask for an encrypted column of type, say, varchar(500), the server isn't
able to enforce that anyway, so we could just prohibit specifying a
nondefault typmod for encrypted columns.

I'm not sure if there are weird types that use typmods in some way where
this wouldn't work. But so far I could not think of anything.

I'll look into this some more.

I thought we often treated atttypid, atttypmod, and attcollation as a
trio, these days. It seems a bit surprising that you'd end up adding
columns for two out of the three.

Internally, we use all three. But for reporting to the client
(RowDescription message), we only have slots for type and typmod. We
could in theory extend the protocol to report the collation as well, but
it's probably not too interesting.

#86Andres Freund
andres@anarazel.de
In reply to: Peter Eisentraut (#83)
Re: Transparent column encryption

Hi,

On 2023-03-23 14:54:48 +0100, Peter Eisentraut wrote:

On 22.03.23 10:00, Peter Eisentraut wrote:

I get that for the type, but why do we need the typmod duplicated as
well?

Earlier patch versions didn't do that, but that got really confusing
about which type the typmod really belonged to, since code currently
assumes that typid+typmod makes sense.� Earlier patch versions had three
fields (usertypid, keyid, encalg), and then I changed it to (usertypid,
usertypmod, keyid) and instead placed the encalg into the real typmod,
which made everything much cleaner.

I thought about this some more. I think we could get rid of attusertypmod
and just hardcode it as -1. The idea would be that if you ask for an
encrypted column of type, say, varchar(500), the server isn't able to
enforce that anyway, so we could just prohibit specifying a nondefault
typmod for encrypted columns.

Why not just use typmod for the underlying typmod? It doesn't seem like
encrypted datums will need that? Or are you using it for something important there?

Greetings,

Andres Freund

#87Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andres Freund (#86)
Re: Transparent column encryption

On 24.03.23 19:12, Andres Freund wrote:

I thought about this some more. I think we could get rid of attusertypmod
and just hardcode it as -1. The idea would be that if you ask for an
encrypted column of type, say, varchar(500), the server isn't able to
enforce that anyway, so we could just prohibit specifying a nondefault
typmod for encrypted columns.

Why not just use typmod for the underlying typmod? It doesn't seem like
encrypted datums will need that? Or are you using it for something important there?

Yes, the typmod of encrypted types stores the encryption algorithm.

(Also, mixing a type with the typmod of another type is weird in a
variety of ways, so this is a quite clean solution.)

#88Andres Freund
andres@anarazel.de
In reply to: Peter Eisentraut (#87)
Re: Transparent column encryption

Hi,

On 2023-03-29 18:08:29 +0200, Peter Eisentraut wrote:

On 24.03.23 19:12, Andres Freund wrote:

I thought about this some more. I think we could get rid of attusertypmod
and just hardcode it as -1. The idea would be that if you ask for an
encrypted column of type, say, varchar(500), the server isn't able to
enforce that anyway, so we could just prohibit specifying a nondefault
typmod for encrypted columns.

Why not just use typmod for the underlying typmod? It doesn't seem like
encrypted datums will need that? Or are you using it for something important there?

Yes, the typmod of encrypted types stores the encryption algorithm.

Why isn't that an attribute of pg_colenckey, given that attcek has been added
to pg_attribute?

(Also, mixing a type with the typmod of another type is weird in a variety
of ways, so this is a quite clean solution.)

It's not an unrelated type though. It's the actual typmod of the column we're
talking about. I find it a lot less clean to make all non-CEK uses of
pg_attribute pay the price of storing three new fields.

Greetings,

Andres Freund

#89Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andres Freund (#88)
Re: Transparent column encryption

On 29.03.23 18:24, Andres Freund wrote:

Hi,

On 2023-03-29 18:08:29 +0200, Peter Eisentraut wrote:

On 24.03.23 19:12, Andres Freund wrote:

I thought about this some more. I think we could get rid of attusertypmod
and just hardcode it as -1. The idea would be that if you ask for an
encrypted column of type, say, varchar(500), the server isn't able to
enforce that anyway, so we could just prohibit specifying a nondefault
typmod for encrypted columns.

Why not just use typmod for the underlying typmod? It doesn't seem like
encrypted datums will need that? Or are you using it for something important there?

Yes, the typmod of encrypted types stores the encryption algorithm.

Why isn't that an attribute of pg_colenckey, given that attcek has been added
to pg_attribute?

One might think that, but the precedent in other equivalent systems is
that you reference the key and the algorithm separately. There is some
(admittedly not very conclusive) discussion about this near [0]/messages/by-id/00b0c4f3-0d9f-dcfd-2ba0-eee5109b4963@enterprisedb.com.

[0]: /messages/by-id/00b0c4f3-0d9f-dcfd-2ba0-eee5109b4963@enterprisedb.com
/messages/by-id/00b0c4f3-0d9f-dcfd-2ba0-eee5109b4963@enterprisedb.com

(Also, mixing a type with the typmod of another type is weird in a variety
of ways, so this is a quite clean solution.)

It's not an unrelated type though. It's the actual typmod of the column we're
talking about.

What I mean is that various parts of the system think that typid+typmod
make sense together. If the typmod actually refers to usertypid, well,
the code doesn't know that, so who knows what happens.

Also, with the current proposal, the system is internally consistent:
pg_encrypted_* can actually look at their own typmod and verify their
own input values that way, which is what a typmod is for. This just
works out of the box.

I find it a lot less clean to make all non-CEK uses of
pg_attribute pay the price of storing three new fields.

With the proposed removal of usertypmod, it's only two fields: the link
to the key and the user-facing type.

#90Andres Freund
andres@anarazel.de
In reply to: Peter Eisentraut (#89)
Re: Transparent column encryption

Hi,

On 2023-03-29 19:08:25 +0200, Peter Eisentraut wrote:

On 29.03.23 18:24, Andres Freund wrote:

On 2023-03-29 18:08:29 +0200, Peter Eisentraut wrote:

On 24.03.23 19:12, Andres Freund wrote:

I thought about this some more. I think we could get rid of attusertypmod
and just hardcode it as -1. The idea would be that if you ask for an
encrypted column of type, say, varchar(500), the server isn't able to
enforce that anyway, so we could just prohibit specifying a nondefault
typmod for encrypted columns.

Why not just use typmod for the underlying typmod? It doesn't seem like
encrypted datums will need that? Or are you using it for something important there?

Yes, the typmod of encrypted types stores the encryption algorithm.

Why isn't that an attribute of pg_colenckey, given that attcek has been added
to pg_attribute?

One might think that, but the precedent in other equivalent systems is that
you reference the key and the algorithm separately. There is some
(admittedly not very conclusive) discussion about this near [0].

[0]: /messages/by-id/00b0c4f3-0d9f-dcfd-2ba0-eee5109b4963@enterprisedb.com

I'm very much not convinced by that. Either way, there at least there should
be a comment mentioning that we intentionally try to allow that.

Even if this feature is something we want (why?), ISTM that this should not be
implemented by having multiple fields in pg_attribute, but instead by a table
referenced by by pg_attribute.attcek.

(Also, mixing a type with the typmod of another type is weird in a variety
of ways, so this is a quite clean solution.)

It's not an unrelated type though. It's the actual typmod of the column we're
talking about.

What I mean is that various parts of the system think that typid+typmod make
sense together. If the typmod actually refers to usertypid, well, the code
doesn't know that, so who knows what happens.

You control what the typmod for encrypted columns does. So I don't see what
problems that could be.

I seems quite likely that having a separate typmod for the encrypted type will
cause problems down the line, because you'll end up having to copy around
typid+typmod for the encrypted datum and then also separately for the
unencrypted one.

Also, with the current proposal, the system is internally consistent:
pg_encrypted_* can actually look at their own typmod and verify their own
input values that way, which is what a typmod is for. This just works out
of the box.

I find it a lot less clean to make all non-CEK uses of
pg_attribute pay the price of storing three new fields.

With the proposed removal of usertypmod, it's only two fields: the link to
the key and the user-facing type.

That feels far less clean. I think loosing the ability to set the precision of
a numeric, or the SRID for postgis datums won't be received very well?

Greetings,

Andres Freund

#91Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andres Freund (#90)
Re: Transparent column encryption

On 30.03.23 03:29, Andres Freund wrote:

One might think that, but the precedent in other equivalent systems is that
you reference the key and the algorithm separately. There is some
(admittedly not very conclusive) discussion about this near [0].

[0]: /messages/by-id/00b0c4f3-0d9f-dcfd-2ba0-eee5109b4963@enterprisedb.com

I'm very much not convinced by that. Either way, there at least there should
be a comment mentioning that we intentionally try to allow that.

Even if this feature is something we want (why?), ISTM that this should not be
implemented by having multiple fields in pg_attribute, but instead by a table
referenced by by pg_attribute.attcek.

I don't know if it is clear to everyone here, but the key data model and
the surrounding DDL are exact copies of the equivalent MS SQL Server
feature. When I was first studying it, I had the exact same doubts
about this. But as I was learning more about it, it does make sense,
because this matches a common pattern in key management systems, which
is relevant because these keys ultimately map into KMS-managed keys in a
deployment. Moreover, 1) it is plausible that those people knew what
they were doing, and 2) it would be preferable to maintain alignment and
not create something that looks the same but is different in some small
but important details.

With the proposed removal of usertypmod, it's only two fields: the link to
the key and the user-facing type.

That feels far less clean. I think loosing the ability to set the precision of
a numeric, or the SRID for postgis datums won't be received very well?

In my mind, and I probably wasn't explicit about this, I'm thinking
about what can be done now versus later.

The feature is arguably useful without typmod support, e.g., for text.
We could ship it like that, then do some work to reorganize pg_attribute
and tuple descriptors to relieve some pressure on each byte, and then
add the typmod support back in in a future release. I think that is a
workable compromise.

#92Andres Freund
andres@anarazel.de
In reply to: Peter Eisentraut (#91)
Re: Transparent column encryption

Hi,

On 2023-03-30 16:01:46 +0200, Peter Eisentraut wrote:

On 30.03.23 03:29, Andres Freund wrote:

One might think that, but the precedent in other equivalent systems is that
you reference the key and the algorithm separately. There is some
(admittedly not very conclusive) discussion about this near [0].

[0]: /messages/by-id/00b0c4f3-0d9f-dcfd-2ba0-eee5109b4963@enterprisedb.com

I'm very much not convinced by that. Either way, there at least there should
be a comment mentioning that we intentionally try to allow that.

Even if this feature is something we want (why?), ISTM that this should not be
implemented by having multiple fields in pg_attribute, but instead by a table
referenced by by pg_attribute.attcek.

I don't know if it is clear to everyone here, but the key data model and the
surrounding DDL are exact copies of the equivalent MS SQL Server feature.
When I was first studying it, I had the exact same doubts about this. But
as I was learning more about it, it does make sense, because this matches a
common pattern in key management systems, which is relevant because these
keys ultimately map into KMS-managed keys in a deployment. Moreover, 1) it
is plausible that those people knew what they were doing, and 2) it would be
preferable to maintain alignment and not create something that looks the
same but is different in some small but important details.

I find it very hard to belief that details of the catalog representation like
this will matter to users. How would would it conceivably affect users that we
store (key, encryption method) in pg_attribute vs storing an oid that's
effectively a foreign key reference to (key, encryption method)?

With the proposed removal of usertypmod, it's only two fields: the link to
the key and the user-facing type.

That feels far less clean. I think loosing the ability to set the precision of
a numeric, or the SRID for postgis datums won't be received very well?

In my mind, and I probably wasn't explicit about this, I'm thinking about
what can be done now versus later.

The feature is arguably useful without typmod support, e.g., for text. We
could ship it like that, then do some work to reorganize pg_attribute and
tuple descriptors to relieve some pressure on each byte, and then add the
typmod support back in in a future release. I think that is a workable
compromise.

I doubt that shipping a version of column encryption that breaks our type
system is a good idea.

Greetings,

Andres Freund

#93Stephen Frost
sfrost@snowman.net
In reply to: Andres Freund (#92)
Re: Transparent column encryption

Greetings,

* Andres Freund (andres@anarazel.de) wrote:

On 2023-03-30 16:01:46 +0200, Peter Eisentraut wrote:

On 30.03.23 03:29, Andres Freund wrote:

One might think that, but the precedent in other equivalent systems is that
you reference the key and the algorithm separately. There is some
(admittedly not very conclusive) discussion about this near [0].

[0]: /messages/by-id/00b0c4f3-0d9f-dcfd-2ba0-eee5109b4963@enterprisedb.com

I'm very much not convinced by that. Either way, there at least there should
be a comment mentioning that we intentionally try to allow that.

Even if this feature is something we want (why?), ISTM that this should not be
implemented by having multiple fields in pg_attribute, but instead by a table
referenced by by pg_attribute.attcek.

I don't know if it is clear to everyone here, but the key data model and the
surrounding DDL are exact copies of the equivalent MS SQL Server feature.
When I was first studying it, I had the exact same doubts about this. But
as I was learning more about it, it does make sense, because this matches a
common pattern in key management systems, which is relevant because these
keys ultimately map into KMS-managed keys in a deployment. Moreover, 1) it
is plausible that those people knew what they were doing, and 2) it would be
preferable to maintain alignment and not create something that looks the
same but is different in some small but important details.

I was wondering about this- is it really exactly the same, down to the
point that there's zero checking of what the data returned actually is
after it's decrypted and given to the application, and if it actually
matches the claimed data type?

I find it very hard to belief that details of the catalog representation like
this will matter to users. How would would it conceivably affect users that we
store (key, encryption method) in pg_attribute vs storing an oid that's
effectively a foreign key reference to (key, encryption method)?

I do agree with this.

With the proposed removal of usertypmod, it's only two fields: the link to
the key and the user-facing type.

That feels far less clean. I think loosing the ability to set the precision of
a numeric, or the SRID for postgis datums won't be received very well?

In my mind, and I probably wasn't explicit about this, I'm thinking about
what can be done now versus later.

The feature is arguably useful without typmod support, e.g., for text. We
could ship it like that, then do some work to reorganize pg_attribute and
tuple descriptors to relieve some pressure on each byte, and then add the
typmod support back in in a future release. I think that is a workable
compromise.

I doubt that shipping a version of column encryption that breaks our type
system is a good idea.

And this.

I do feel that column encryption is a useful capability and there's
large parts of this approach that I agree with, but I dislike the idea
of having our clients be able to depend on what gets returned for
non-encrypted columns while not being able to trust what encrypted
column results are and then trying to say it's 'transparent'. To that
end, it seems like just saying they get back a bytea and making it clear
that they have to provide the validation would be clear, while keeping
much of the rest. Expanding out from that I'd imagine, pie-in-the-sky
and in some far off land, having our data type in/out validation
functions moved to the common library and then adding client-side
validation of the data going in/out of the encrypted columns would allow
application developers to be able to trust what we're returning (as long
as they're using libpq- and we'd have to document that independent
implementations of the protocol have to provide this or just continue to
return bytea's).

Not sure how we'd manage to provide support for extensions though.

Thanks,

Stephen

#94Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Andres Freund (#92)
Re: Transparent column encryption

On 30.03.23 17:55, Andres Freund wrote:

I find it very hard to belief that details of the catalog representation like
this will matter to users. How would would it conceivably affect users that we
store (key, encryption method) in pg_attribute vs storing an oid that's
effectively a foreign key reference to (key, encryption method)?

The change you are alluding to would also affect how the DDL commands
work and interoperate, so it affects the user.

But also, let's not drive this design decision bottom up. Let's go from
how we want the data model and the DDL to work and then figure out
suitable ways to record that. I don't really know if you are just
worried about the catalog size, or you find an actual fault with the
data model, or you just find it subjectively odd.

The feature is arguably useful without typmod support, e.g., for text. We
could ship it like that, then do some work to reorganize pg_attribute and
tuple descriptors to relieve some pressure on each byte, and then add the
typmod support back in in a future release. I think that is a workable
compromise.

I doubt that shipping a version of column encryption that breaks our type
system is a good idea.

I don't follow how you get from that to claiming that it breaks the type
system. Please provide more details.

#95Peter Eisentraut
peter.eisentraut@enterprisedb.com
In reply to: Stephen Frost (#93)
Re: Transparent column encryption

On 30.03.23 20:35, Stephen Frost wrote:

I do feel that column encryption is a useful capability and there's
large parts of this approach that I agree with, but I dislike the idea
of having our clients be able to depend on what gets returned for
non-encrypted columns while not being able to trust what encrypted
column results are and then trying to say it's 'transparent'. To that
end, it seems like just saying they get back a bytea and making it clear
that they have to provide the validation would be clear, while keeping
much of the rest.

[Note that the word "transparent" has been removed from the feature
name. I just didn't change the email thread name.]

These thoughts are reasonable, but I think there is a tradeoff to be
made between having featureful data validation and enhanced security.
If you want your database system to validate your data, you have to send
it in plain text. If you want to have your database system not see the
plain text, then it cannot validate it. But there is still utility in it.

You can't really depend on what gets returned even in the non-encrypted
case, unless you cryptographically sign the schema against modification
or something like that. So realistically, a client that cares strongly
about the data it receives has to do some kind of client-side validation
anyway.

Note also that higher-level client libraries like JDBC effectively do
client-side data validation, for example when you call
ResultSet.getInt() etc.

This is also one of the reasons why the user facing type declaration
exists. You could just make all encrypted columns of type "opaque" or
something and not make any promises about what's inside. But client
APIs sort or rely on the application being able to ask the result set
for what's inside a column value. If we just say, we don't know, then
applications (or driver APIs) will have to be changed to accommodate
that, but the intention was to not require that. So instead we say,
it's supposed to be int, and then if it's sometimes actually not int,
then your application throws an exception you can deal with. This is
arguably a better developer experience, even if it concerns the data
type purist.

But do you have a different idea about how it should work?

Expanding out from that I'd imagine, pie-in-the-sky
and in some far off land, having our data type in/out validation
functions moved to the common library and then adding client-side
validation of the data going in/out of the encrypted columns would allow
application developers to be able to trust what we're returning (as long
as they're using libpq- and we'd have to document that independent
implementations of the protocol have to provide this or just continue to
return bytea's).

As mentioned, some client libraries effectively already do that. But
even if we could make this much more comprehensive, I don't see how this
can ever actually satisfy your point. It would require that all
participating clients apply validation all the time, and all other
clients can rely on that happening, which is impossible.

#96Peter Eisentraut
peter@eisentraut.org
In reply to: Peter Eisentraut (#78)
1 attachment(s)
Re: Transparent column encryption

To kick some things off for PG18, here is an updated version of the
patch for automatic client-side column-level encryption. (See commit
message included in the patch for a detailed description, if you have
forgotten. Also, see [0]/messages/by-id/89157929-c2b6-817b-6025-8e4b2d89d88f@enterprisedb.com if the thread has dropped off your local mail
storage.)

[0]: /messages/by-id/89157929-c2b6-817b-6025-8e4b2d89d88f@enterprisedb.com
/messages/by-id/89157929-c2b6-817b-6025-8e4b2d89d88f@enterprisedb.com

This patch got stuck around CF 2023-03 because expanding the size of the
tuple descriptor (with new pg_attribute columns) had a noticeable
performance impact. Various work in PG17 has made it more manageable to
have columns in pg_attribute that are not in the tuple descriptor, and
this patch now takes advantage of that (and I wanted to do this merge
soon to verify that the changes in PG17 are usable). Otherwise, this
version v20 is functionally unchanged over the last posted version v19.
Obviously, it's early days, so there will be plenty of time to have
discussions on various other aspects of this patch. I'm keeping a keen
eye on the discussion of protocol extensions, for example.

Attachments:

v20-0001-Automatic-client-side-column-level-encryption.patchtext/plain; charset=UTF-8; name=v20-0001-Automatic-client-side-column-level-encryption.patchDownload
From e08b49b39a572c7816a430a576e1a145a965d60a Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 10 Apr 2024 11:52:58 +0200
Subject: [PATCH v20] Automatic client-side column-level encryption

This feature enables the automatic 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 asymmetric 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
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.

To get automatically encrypted data into the database (as opposed to
reading it out), it is required to use protocol-level prepared
statements (i.e., extended query).  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.  libpq's
PQexecParams() does this internally.  For the asynchronous interfaces,
additional libpq functions are added to be able to pass the describe
result back into the statement execution function.  (Other client APIs
that have a "statement handle" concept could do this more elegantly
and probably without any API changes.)  psql also supports this if the
\bind command is used.

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.

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                    | 317 +++++++
 doc/src/sgml/charset.sgml                     |  10 +
 doc/src/sgml/datatype.sgml                    |  61 ++
 doc/src/sgml/ddl.sgml                         | 444 +++++++++
 doc/src/sgml/func.sgml                        |  60 ++
 doc/src/sgml/glossary.sgml                    |  26 +
 doc/src/sgml/libpq.sgml                       | 322 +++++++
 doc/src/sgml/protocol.sgml                    | 468 ++++++++++
 doc/src/sgml/ref/allfiles.sgml                |   6 +
 .../sgml/ref/alter_column_encryption_key.sgml | 197 ++++
 doc/src/sgml/ref/alter_column_master_key.sgml | 134 +++
 doc/src/sgml/ref/comment.sgml                 |   2 +
 doc/src/sgml/ref/copy.sgml                    |  10 +
 .../ref/create_column_encryption_key.sgml     | 173 ++++
 .../sgml/ref/create_column_master_key.sgml    | 107 +++
 doc/src/sgml/ref/create_table.sgml            |  55 +-
 doc/src/sgml/ref/discard.sgml                 |  14 +-
 .../sgml/ref/drop_column_encryption_key.sgml  | 112 +++
 doc/src/sgml/ref/drop_column_master_key.sgml  | 112 +++
 doc/src/sgml/ref/grant.sgml                   |  12 +-
 doc/src/sgml/ref/pg_dump.sgml                 |  42 +
 doc/src/sgml/ref/pg_dumpall.sgml              |  27 +
 doc/src/sgml/ref/psql-ref.sgml                |  39 +
 doc/src/sgml/reference.sgml                   |   6 +
 src/backend/access/common/printsimple.c       |   8 +
 src/backend/access/common/printtup.c          | 236 ++++-
 src/backend/access/hash/hashvalidate.c        |   2 +-
 src/backend/bootstrap/bootparse.y             |   1 +
 src/backend/catalog/aclchk.c                  |  60 ++
 src/backend/catalog/dependency.c              |   6 +
 src/backend/catalog/heap.c                    |  61 +-
 src/backend/catalog/index.c                   |   4 +
 src/backend/catalog/namespace.c               | 272 ++++++
 src/backend/catalog/objectaddress.c           | 287 ++++++
 src/backend/catalog/toasting.c                |   1 +
 src/backend/commands/Makefile                 |   1 +
 src/backend/commands/alter.c                  |  14 +
 src/backend/commands/cluster.c                |   1 +
 src/backend/commands/colenccmds.c             | 449 ++++++++++
 src/backend/commands/createas.c               |  30 +
 src/backend/commands/discard.c                |   8 +-
 src/backend/commands/dropcmds.c               |  15 +
 src/backend/commands/event_trigger.c          |   6 +
 src/backend/commands/meson.build              |   1 +
 src/backend/commands/seclabel.c               |   3 +
 src/backend/commands/tablecmds.c              | 239 ++++-
 src/backend/commands/variable.c               |   7 +-
 src/backend/commands/view.c                   |  22 +-
 src/backend/nodes/nodeFuncs.c                 |   2 +
 src/backend/parser/gram.y                     | 194 +++-
 src/backend/parser/parse_param.c              | 157 ++++
 src/backend/parser/parse_relation.c           |   2 +-
 src/backend/parser/parse_utilcmd.c            |  14 +
 src/backend/tcop/backend_startup.c            |  19 +-
 src/backend/tcop/postgres.c                   |  64 ++
 src/backend/tcop/utility.c                    |  56 ++
 src/backend/utils/adt/acl.c                   | 398 +++++++++
 src/backend/utils/adt/varlena.c               | 107 +++
 src/backend/utils/cache/lsyscache.c           |  83 ++
 src/backend/utils/cache/plancache.c           |   4 +-
 src/backend/utils/mb/mbutils.c                |  18 +-
 src/bin/pg_dump/common.c                      |  44 +
 src/bin/pg_dump/dumputils.c                   |   4 +
 src/bin/pg_dump/pg_backup.h                   |   1 +
 src/bin/pg_dump/pg_backup_archiver.c          |   2 +
 src/bin/pg_dump/pg_backup_db.c                |   9 +-
 src/bin/pg_dump/pg_dump.c                     | 416 ++++++++-
 src/bin/pg_dump/pg_dump.h                     |  35 +
 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              | 109 +++
 src/bin/psql/command.c                        |   6 +-
 src/bin/psql/describe.c                       | 191 +++-
 src/bin/psql/describe.h                       |   6 +
 src/bin/psql/help.c                           |   4 +
 src/bin/psql/settings.h                       |   1 +
 src/bin/psql/startup.c                        |  10 +
 src/bin/psql/tab-complete.c                   |  72 +-
 src/common/Makefile                           |   1 +
 src/common/colenc.c                           | 107 +++
 src/common/meson.build                        |   1 +
 src/include/access/printtup.h                 |   4 +
 src/include/catalog/Makefile                  |   5 +-
 src/include/catalog/heap.h                    |   4 +-
 src/include/catalog/meson.build               |   3 +
 src/include/catalog/namespace.h               |   6 +
 src/include/catalog/pg_amop.dat               |   5 +
 src/include/catalog/pg_amproc.dat             |   6 +
 src/include/catalog/pg_attribute.h            |  14 +-
 src/include/catalog/pg_colenckey.h            |  49 +
 src/include/catalog/pg_colenckeydata.h        |  49 +
 src/include/catalog/pg_colmasterkey.h         |  50 ++
 src/include/catalog/pg_opclass.dat            |   2 +
 src/include/catalog/pg_operator.dat           |  15 +
 src/include/catalog/pg_opfamily.dat           |   2 +
 src/include/catalog/pg_proc.dat               | 100 +++
 src/include/catalog/pg_type.dat               |  13 +
 src/include/catalog/pg_type.h                 |   1 +
 src/include/commands/colenccmds.h             |  26 +
 src/include/commands/tablecmds.h              |   4 +-
 src/include/common/colenc.h                   |  51 ++
 src/include/libpq/libpq-be.h                  |   1 +
 src/include/libpq/protocol.h                  |   2 +
 src/include/nodes/parsenodes.h                |  41 +-
 src/include/parser/kwlist.h                   |   2 +
 src/include/parser/parse_param.h              |   1 +
 src/include/tcop/cmdtaglist.h                 |   7 +
 src/include/utils/acl.h                       |   2 +
 src/include/utils/lsyscache.h                 |   4 +
 src/include/utils/plancache.h                 |   3 +
 src/interfaces/libpq/Makefile                 |   1 +
 src/interfaces/libpq/exports.txt              |   4 +
 src/interfaces/libpq/fe-connect.c             |  46 +
 src/interfaces/libpq/fe-encrypt-openssl.c     | 840 ++++++++++++++++++
 src/interfaces/libpq/fe-encrypt.h             |  33 +
 src/interfaces/libpq/fe-exec.c                | 674 +++++++++++++-
 src/interfaces/libpq/fe-protocol3.c           | 157 +++-
 src/interfaces/libpq/fe-trace.c               |  56 +-
 src/interfaces/libpq/libpq-fe.h               |  20 +
 src/interfaces/libpq/libpq-int.h              |  37 +
 src/interfaces/libpq/meson.build              |   2 +
 src/interfaces/libpq/nls.mk                   |   1 +
 src/interfaces/libpq/t/010_encrypt.pl         |  72 ++
 src/interfaces/libpq/test/.gitignore          |   1 +
 src/interfaces/libpq/test/Makefile            |   7 +
 src/interfaces/libpq/test/meson.build         |  23 +
 src/test/Makefile                             |   4 +-
 src/test/column_encryption/.gitignore         |   3 +
 src/test/column_encryption/Makefile           |  31 +
 src/test/column_encryption/meson.build        |  23 +
 .../t/001_column_encryption.pl                | 307 +++++++
 .../column_encryption/t/002_cmk_rotation.pl   | 125 +++
 src/test/column_encryption/test_client.c      | 161 ++++
 .../column_encryption/test_run_decrypt.pl     |  61 ++
 src/test/meson.build                          |   1 +
 .../regress/expected/column_encryption.out    | 541 +++++++++++
 src/test/regress/expected/object_address.out  |  35 +
 src/test/regress/expected/oidjoins.out        |   8 +
 src/test/regress/expected/opr_sanity.out      |  12 +-
 src/test/regress/expected/type_sanity.out     |   6 +-
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/pg_regress_main.c            |   2 +-
 src/test/regress/sql/column_encryption.sql    | 371 ++++++++
 src/test/regress/sql/object_address.sql       |  11 +
 src/test/regress/sql/type_sanity.sql          |   2 +
 src/tools/pgindent/typedefs.list              |   9 +
 147 files changed, 10710 insertions(+), 115 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/common/colenc.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/include/common/colenc.h
 create mode 100644 src/interfaces/libpq/fe-encrypt-openssl.c
 create mode 100644 src/interfaces/libpq/fe-encrypt.h
 create mode 100644 src/interfaces/libpq/t/010_encrypt.pl
 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 817d062c7e6..ecdd0cc602a 100644
--- a/doc/src/sgml/acronyms.sgml
+++ b/doc/src/sgml/acronyms.sgml
@@ -65,6 +65,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CEK</acronym></term>
+    <listitem>
+     <para>
+      Column Encryption Key; see <xref linkend="ddl-column-encryption"/>
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><acronym>CIDR</acronym></term>
     <listitem>
@@ -76,6 +85,15 @@ <title>Acronyms</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><acronym>CMK</acronym></term>
+    <listitem>
+     <para>
+      Column Master Key; see <xref linkend="ddl-column-encryption"/>
+     </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 096ddab481c..f03fc63f764 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>
@@ -1410,6 +1425,44 @@ <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 null.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypid</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.  If the column is not
+       encrypted, then null.  For encrypted columns, the field
+       <structfield>atttypid</structfield> is either
+       <type>pg_encrypted_det</type> or <type>pg_encrypted_rnd</type>.
+       </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attusertypmod</structfield> <type>int4</type>
+      </para>
+      <para>
+       If the column is encrypted, then this column indicates the type
+       modifier (analogous to <structfield>atttypmod</structfield>) that is
+       reported to the client.  If the column is not encrypted, then null.  For
+       encrypted columns, the field <structfield>atttypmod</structfield>)
+       contains 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>attmissingval</structfield> <type>anyarray</type>
@@ -2494,6 +2547,270 @@ <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>ceknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column encryption key
+      </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>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cekacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </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>int4</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>cmknamespace</structfield> <type>oid</type>
+       (references <link linkend="catalog-pg-namespace"><structname>pg_namespace</structname></link>.<structfield>oid</structfield>)
+      </para>
+      <para>
+       The OID of the namespace that contains this column master key
+      </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>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>cmkacl</structfield> <type>aclitem[]</type>
+      </para>
+      <para>
+       Access privileges; see <xref linkend="ddl-priv"/> for details
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect1>
+
  <sect1 id="catalog-pg-constraint">
   <title><structname>pg_constraint</structname></title>
 
diff --git a/doc/src/sgml/charset.sgml b/doc/src/sgml/charset.sgml
index 55bbb20dacc..021b2fffc7b 100644
--- a/doc/src/sgml/charset.sgml
+++ b/doc/src/sgml/charset.sgml
@@ -2397,6 +2397,16 @@ <title>Automatic Character Set Conversion Between Server and Client</title>
      Just as for the server, use of <literal>SQL_ASCII</literal> is unwise
      unless you are working with all-ASCII data.
     </para>
+
+    <para>
+     When automatic client-side column-level encryption is used, then no
+     encoding conversion is possible.  (The encoding conversion happens on the
+     server, and the server cannot look inside any encrypted column values.)
+     If automatic client-side column-level encryption is enabled for a
+     session, then the server enforces that the client encoding matches the
+     server encoding, and any attempts to change the client encoding will be
+     rejected by the server.
+    </para>
    </sect2>
 
    <sect2 id="multibyte-conversions-supported">
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 73e51b0b114..49203a86806 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5390,4 +5390,65 @@ <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
+    these as normal types.  For example, the type <type>pg_encrypted_det</type> has
+    an equals operator that allows lookup of encrypted values.  It is,
+    however, not allowed to create a table using one of these types directly
+    as a column type.
+   </para>
+
+   <para>
+    The external representation of these types is the string
+    <literal>encrypted$</literal> followed by hexadecimal byte values, for
+    example
+    <literal>encrypted$3aacd063d2d3a1a04119df76874e0b9785ea466177f18fe9c0a1a313eaf09c98</literal>.
+    Clients that don't support automatic client-side column-level encryption
+    or have disabled it will see the encrypted values in this format.  Clients
+    that support automatic client-side column-level encryption will not see
+    these types in result sets, as the protocol layer will translate them back
+    to the 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>
+
+    <para>
+     For encrypted columns, the type modifier (<structfield>atttypmod</structfield>)
+     contains the identifier of the encryption algorithm; see <xref
+     linkend="protocol-cek"/> for possible values.
+    </para>
+  </sect1>
  </chapter>
diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 00f44f56faf..2182c6c7865 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1375,6 +1375,440 @@ <title>Exclusion Constraints</title>
   </sect2>
  </sect1>
 
+ <sect1 id="ddl-column-encryption">
+  <title>Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   With <firstterm>automatic client-side column-level encryption</firstterm>,
+   columns can be stored encrypted in the database.  The encryption and
+   decryption happens automatically 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 id="ddl-column-encryption-using">
+   <title>Using Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   Automatic client-side column-level 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 asymmetric 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 from making
+   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 automatic client-side column-level
+   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 id="ddl-column-encryption-reading-writing-encrypted-columns">
+   <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 automatic client-side column-level
+       encryption.  Not all client libraries do.  Furthermore, the client
+       library might require that automatic client-side column-level
+       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>
+    would 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 employees (id, name, ssn) VALUES (1, 'Someone', '12345');
+</programlisting>
+    This would leak the unencrypted value <literal>12345</literal> to the
+    server, thus defeating the point of client-side column-level encryption.
+    (And even ignoring that, it could not work because the server does not
+    have access to the keys to perform the 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>
+    This shows a correct invocation in libpq (without error checking):
+<programlisting>
+PGresult   *res;
+const char *values[] = {"1", "Someone", "12345"};
+
+res = PQexecParams(conn, "INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3)",
+                   3, NULL, values, NULL, NULL, 0);
+</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>\bind</literal> to run statements with parameters like this:
+<programlisting>
+INSERT INTO employees (id, name, ssn) VALUES ($1, $2, $3) \bind '1' 'Someone', '12345' \g
+</programlisting>
+   </para>
+
+   <para>
+    Similarly, if deterministic encryption is used, parameters need to be used
+    in search conditions using encrypted columns:
+<programlisting>
+SELECT * FROM employees WHERE ssn = $1 \bind '12345' \g
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2 id="ddl-column-encryption-setting-up">
+   <title>Setting up Automatic Client-side Column-level Encryption</title>
+
+  <para>
+   The steps to set up automatic client-side column-level 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"
+</programlisting>
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      Create the unencrypted CEK key material in a file:
+<programlisting>
+openssl rand -out cek1.bin 48
+</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, algorithm = 'RSAES_OAEP_SHA_1', 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 automatic client-side column-level 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 id="ddl-column-encryption-guidance">
+   <title>Guidance on Using Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    This section contains some information on when it is or is not appropriate
+    to use automatic client-side column-level 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 transport 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 allows 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>
+    When using parameters to provide values to insert or search by, care must
+    be taken that values meant to be encrypted are not accidentally leaked to
+    the server.  The server will tell the client which parameters to encrypt,
+    based on the schema definition on the server.  But if the query or client
+    application is faulty, values meant to be encrypted might accidentally be
+    associated with parameters that the server does not think need to be
+    encrypted.  Additional robustness can be achieved by forcing encryption of
+    certain parameters in the client library (see its documentation; for
+    <application>libpq</application>, see <xref
+    linkend="libpq-connect-column-encryption"/>).
+   </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, 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.  Note that column encryption is applied
+    to the text representation of the stored value, so length differences can
+    be leaked even for fixed-length column types (e.g.  <type>bigint</type>,
+    whose largest decimal representation is longer than 16 bytes).
+   </para>
+
+   <para>
+    Column encryption provides only partial protection against a malicious
+    user with write access to the table.  Once encrypted, any modifications to
+    a stored value on the server side will cause a decryption failure on the
+    client.  However, a user with write access can still freely swap encrypted
+    values between rows or columns (or even separate database clusters) as
+    long as they were encrypted with the same key.  Attackers can also remove
+    values by replacing them with nulls, and users with ownership over the
+    table schema can replace encryption keys or strip encryption from the
+    columns entirely.  All of this is to say: Proper access control is still
+    of vital importance when using this feature.
+   </para>
+
+   <tip>
+    <para>
+     One might be inclined to think of the client-side column-level encryption
+     feature as a mechanism for application writers and users to protect
+     themselves against an <quote>evil DBA</quote>, but that is not the
+     intended purpose.  Rather, it is (also) a tool for the DBA to control
+     which data they do not want (in plaintext) on the server.
+    </para>
+   </tip>
+
+   <para>
+    When using asymmetric CMK algorithms to encrypt CEKs, the
+    <quote>public</quote> half of the CMK can be used to replace existing
+    column encryption keys with keys of an attacker's choosing, compromising
+    confidentiality and authenticity for values encrypted under that CMK.  For
+    this reason, it's important to keep both the private
+    <emphasis>and</emphasis> public halves of the CMK key pair confidential.
+   </para>
+
+   <note>
+    <para>
+     Storing data such credit card data, medical data, and so on 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>
 
@@ -2136,6 +2570,14 @@ <title>Privileges</title>
        server.  Grantees may also create, alter, or drop their own user
        mappings associated with that server.
       </para>
+      <para>
+       For column master keys, allows the creation of column encryption keys
+       using the master key.
+      </para>
+      <para>
+       For column encryption keys, allows the use of the key in the creation
+       of table columns.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2302,6 +2744,8 @@ <title>ACL Privilege Abbreviations</title>
       <entry><literal>USAGE</literal></entry>
       <entry><literal>U</literal></entry>
       <entry>
+       <literal>COLUMN ENCRYPTION KEY</literal>,
+       <literal>COLUMN MASTER KEY</literal>,
        <literal>DOMAIN</literal>,
        <literal>FOREIGN DATA WRAPPER</literal>,
        <literal>FOREIGN SERVER</literal>,
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index bf13216e477..77e7ee0bbd0 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -24896,6 +24896,40 @@ <title>Access Privilege Inquiry Functions</title>
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_encryption_key_privilege</primary>
+        </indexterm>
+        <function>has_column_encryption_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cek</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column encryption key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>has_column_master_key_privilege</primary>
+        </indexterm>
+        <function>has_column_master_key_privilege</function> (
+          <optional> <parameter>user</parameter> <type>name</type> or <type>oid</type>, </optional>
+          <parameter>cmk</parameter> <type>text</type> or <type>oid</type>,
+          <parameter>privilege</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Does user have privilege for column master key?
+        The only allowable privilege type is <literal>USAGE</literal>.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
@@ -25386,6 +25420,32 @@ <title>Schema Visibility Inquiry Functions</title>
      </thead>
 
      <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cek_is_visible</primary>
+        </indexterm>
+        <function>pg_cek_is_visible</function> ( <parameter>cek</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column encryption key visible in search path?
+       </para></entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_cmk_is_visible</primary>
+        </indexterm>
+        <function>pg_cmk_is_visible</function> ( <parameter>cmk</parameter> <type>oid</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Is column master key visible in search path?
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index a81c17a8690..89e67b6b5c5 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -433,6 +433,32 @@ <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 automatic
+     client-side column-level encryption (<xref
+     linkend="ddl-column-encryption"/>).  Column encryption keys are stored in
+     the database encrypted by another key, the <glossterm
+     linkend="glossary-column-master-key">column master key</glossterm>.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-column-master-key">
+   <glossterm>Column master key</glossterm>
+   <glossdef>
+    <para>
+     A cryptographic key used to encrypt <glossterm
+     linkend="glossary-column-encryption-key">column encryption
+     keys</glossterm>.  (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 35fb346ed97..9f8f2c3c13b 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2262,6 +2262,141 @@ <title>Parameter Key Words</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-column-encryption" xreflabel="column_encryption">
+      <term><literal>column_encryption</literal></term>
+      <listitem>
+       <para>
+        If set to <literal>on</literal>, <literal>true</literal>, or
+        <literal>1</literal>, this enables automatic client-side column-level
+        encryption for the connection.  If encrypted columns are queried and
+        this is not enabled, the encrypted values are 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>%j</literal></term>
+          <listitem>
+           <para>
+            The CMK algorithm name in JSON Web Algorithms format (see <xref
+            linkend="protocol-cmk-table"/>).  This is useful for interfacing
+            with some key management systems that use these names.
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%k</literal></term>
+          <listitem>
+           <para>
+            The CMK key name
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>%p</literal></term>
+          <listitem>
+           <para>
+            The name of a temporary file with the encrypted CEK data (only for
+            the <literal>run</literal> scheme)
+           </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>
+
+           <para>
+            The file scheme does not support the CMK algorithm
+            <literal>unspecified</literal>.
+           </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 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 --infile '%p'"
+</programlisting>
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-load-balance-hosts" xreflabel="load_balance_hosts">
       <term><literal>load_balance_hosts</literal></term>
       <listitem>
@@ -3252,6 +3387,32 @@ <title>Main Functions</title>
             <filename>src/backend/utils/adt/numeric.c::numeric_send()</filename> and
             <filename>src/backend/utils/adt/numeric.c::numeric_recv()</filename>.
            </para>
+
+           <para>
+            When column encryption is enabled, the second-least-significant
+            half-byte of this parameter specifies whether encryption should be
+            forced for a parameter.  Set this half-byte to one to force
+            encryption.  For example, use the C code literal
+            <literal>0x10</literal> to specify text format with forced
+            encryption.  If the array pointer is null then encryption is not
+            forced for any parameter.
+           </para>
+
+           <para>
+            Parameters corresponding to encrypted columns must be passed in
+            text format.  Specifying binary format for such a parameter will
+            result in an error.
+           </para>
+
+           <para>
+            If encryption is forced for a parameter but the parameter 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 compromised 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.)
+           </para>
           </listitem>
          </varlistentry>
 
@@ -3264,6 +3425,13 @@ <title>Main Functions</title>
             to obtain different result columns in different formats,
             although that is possible in the underlying protocol.)
            </para>
+
+           <para>
+            If column encryption is used, then encrypted columns will be
+            returned in text format independent of this setting.  Applications
+            can check the format of each result column with <xref
+            linkend="libpq-PQfformat"/> before accessing it.
+           </para>
           </listitem>
          </varlistentry>
         </variablelist>
@@ -3413,6 +3581,44 @@ <title>Main Functions</title>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-PQexecPreparedDescribed">
+      <term><function>PQexecPreparedDescribed</function><indexterm><primary>PQexecPreparedDescribed</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 *PQexecPreparedDescribed(PGconn *conn,
+                                  const char *stmtName,
+                                  int nParams,
+                                  const char * const *paramValues,
+                                  const int *paramLengths,
+                                  const int *paramFormats,
+                                  int resultFormat,
+                                  PGresult *paramDesc);
+</synopsis>
+       </para>
+
+       <para>
+        <xref linkend="libpq-PQexecPreparedDescribed"/> 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>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-PQdescribePrepared">
       <term><function>PQdescribePrepared</function><indexterm><primary>PQdescribePrepared</primary></indexterm></term>
 
@@ -4341,6 +4547,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>
 
@@ -4522,6 +4750,31 @@ <title>Retrieving Query Result Information</title>
      </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-PQexecPreparedDescribed"/> 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>
 
@@ -5047,6 +5300,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQsendQueryParams"/>,
    <xref linkend="libpq-PQsendPrepare"/>,
    <xref linkend="libpq-PQsendQueryPrepared"/>,
+   <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
    <xref linkend="libpq-PQsendDescribePrepared"/>,
    <xref linkend="libpq-PQsendDescribePortal"/>,
    <xref linkend="libpq-PQsendClosePrepared"/>, and
@@ -5056,6 +5310,7 @@ <title>Asynchronous Command Processing</title>
    <xref linkend="libpq-PQexecParams"/>,
    <xref linkend="libpq-PQprepare"/>,
    <xref linkend="libpq-PQexecPrepared"/>,
+   <xref linkend="libpq-PQexecPreparedDescribed"/>,
    <xref linkend="libpq-PQdescribePrepared"/>,
    <xref linkend="libpq-PQdescribePortal"/>
    <xref linkend="libpq-PQclosePrepared"/>, and
@@ -5114,6 +5369,13 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQexecParams"/>, it allows only one command in the
        query string.
       </para>
+
+      <para>
+       If column encryption is enabled, then this function is not
+       asynchronous.  To get asynchronous behavior, <xref
+       linkend="libpq-PQsendPrepare"/> followed by <xref
+       linkend="libpq-PQsendQueryPreparedDescribed"/> should be called individually.
+      </para>
      </listitem>
     </varlistentry>
 
@@ -5168,6 +5430,45 @@ <title>Asynchronous Command Processing</title>
      </listitem>
     </varlistentry>
 
+    <varlistentry id="libpq-PQsendQueryPreparedDescribed">
+     <term><function>PQsendQueryPreparedDescribed</function><indexterm><primary>PQsendQueryPreparedDescribed</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 PQsendQueryPreparedDescribed(PGconn *conn,
+                                 const char *stmtName,
+                                 int nParams,
+                                 const char * const *paramValues,
+                                 const int *paramLengths,
+                                 const int *paramFormats,
+                                 int resultFormat,
+                                 PGresult *paramDesc);
+</synopsis>
+      </para>
+
+      <para>
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/> 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-PQexecPreparedDescribed"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry id="libpq-PQsendDescribePrepared">
      <term><function>PQsendDescribePrepared</function><indexterm><primary>PQsendDescribePrepared</primary></indexterm></term>
 
@@ -5258,6 +5559,7 @@ <title>Asynchronous Command Processing</title>
        <xref linkend="libpq-PQsendQueryParams"/>,
        <xref linkend="libpq-PQsendPrepare"/>,
        <xref linkend="libpq-PQsendQueryPrepared"/>,
+       <xref linkend="libpq-PQsendQueryPreparedDescribed"/>,
        <xref linkend="libpq-PQsendDescribePrepared"/>,
        <xref linkend="libpq-PQsendDescribePortal"/>,
        <xref linkend="libpq-PQsendClosePrepared"/>,
@@ -8848,6 +9150,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 a8ec72c27f4..e88af6f31f1 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1091,6 +1091,76 @@ <title>Pipelining</title>
    </para>
   </sect2>
 
+  <sect2 id="protocol-flow-column-encryption">
+   <title>Automatic Client-side Column-level Encryption</title>
+
+   <para>
+    Automatic client-side column-level 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 automatic client-side column-level 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 automatic column-level 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, format specifications (text/binary) in the
+    various protocol messages apply to the ciphertext.  The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.  Even though the ciphertext could in theory be sent in either
+    text or binary format, the server will always send it in binary if the
+    column-level encryption protocol option is enabled.  That way, a client
+    library only needs to support decrypting data sent in binary and does not
+    have to support decoding the text format of the encryption-related types
+    (see <xref linkend="datatype-encrypted"/>).
+   </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 automatic client-side column-level 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 automatic client-side column-level
+    encryption are described in <xref
+    linkend="protocol-column-encryption-crypto"/>.
+   </para>
+  </sect2>
+
   <sect2 id="protocol-flow-function-call">
    <title>Function Call</title>
 
@@ -3994,6 +4064,16 @@ <title>Message Formats</title>
          The parameter format codes.  Each must presently be
          zero (text) or one (binary).
         </para>
+
+        <para>
+         If the protocol extension <literal>_pq_.column_encryption</literal>
+         is enabled (see <xref linkend="protocol-flow-column-encryption"/>),
+         then the second-least-significant half-byte is set to one if the
+         parameter was encrypted by the client.  (So, for example, to send an
+         encrypted value in binary, the field is set to 0x11 in total.)  This
+         is used by the server to check that a parameter that was required to
+         be encrypted was actually encrypted.
+        </para>
        </listitem>
       </varlistentry>
 
@@ -4214,6 +4294,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-flow-column-encryption"/>.)
+     </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>Int32</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-flow-column-encryption"/>.)
+     </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>
@@ -5317,6 +5531,45 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), 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>Int32</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 parameter is to be
+         encrypted and bit 0x0001 is set, the column underlying the parameter
+         uses deterministic encryption, otherwise randomized encryption.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -5705,6 +5958,50 @@ <title>Message Formats</title>
        </listitem>
       </varlistentry>
      </variablelist>
+
+     <para>
+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), 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>Int32</term>
+       <listitem>
+        <para>
+         If the field is 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 field is encrypted and
+         bit 0x0001 is set, the field uses deterministic encryption, otherwise
+         randomized encryption.
+        </para>
+        <!--
+            This is not really useful here, but it keeps alignment with
+            ParameterDescription.  Future flags might be useful in both
+            places.
+        -->
+       </listitem>
+      </varlistentry>
+     </variablelist>
     </listitem>
    </varlistentry>
 
@@ -7530,6 +7827,177 @@ <title>Logical Replication Message Formats</title>
   </variablelist>
  </sect1>
 
+ <sect1 id="protocol-column-encryption-crypto">
+  <title>Automatic Client-side Column-level Encryption Cryptography</title>
+
+  <para>
+   This section describes the cryptographic operations used by the automatic
+   client-side column-level encryption functionality.  A client that supports
+   this functionality 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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <table id="protocol-cmk-table">
+    <title>Column Master Key Algorithms</title>
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>JWA (<ulink url="https://datatracker.ietf.org/doc/html/rfc7518">RFC 7518</ulink>) name</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>1</entry>
+       <entry><literal>unspecified</literal></entry>
+       <entry>(none)</entry>
+       <entry>interpreted by client</entry>
+      </row>
+      <row>
+       <entry>2</entry>
+       <entry><literal>RSAES_OAEP_SHA_1</literal></entry>
+       <entry><literal>RSA-OAEP</literal></entry>
+       <entry>RSAES OAEP using default parameters (<ulink
+       url="https://datatracker.ietf.org/doc/html/rfc8017">RFC
+       8017</ulink>/PKCS #1)</entry>
+      </row>
+      <row>
+       <entry>3</entry>
+       <entry><literal>RSAES_OAEP_SHA_256</literal></entry>
+       <entry><literal>RSA-OAEP-256</literal></entry>
+       <entry>RSAES OAEP using SHA-256 and MGF1 with SHA-256 (<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>
+
+   <!-- see also src/include/common/colenc.h -->
+
+   <para>
+    The key material of a column encryption key consists of three components,
+    concatenated in this order: the MAC key, the encryption key, and the IV
+    key.  <xref linkend="protocol-cek-table"/> shows the total length that a
+    key generated for each algorithm is required to have.  The MAC key and the
+    encryption key are used by the referenced encryption algorithms; see there
+    for details.  The IV key is used for computing the static initialization
+    vector for deterministic encryption; it is unused for randomized
+    encryption.
+   </para>
+
+   <!-- see also https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-2.8 -->
+   <table id="protocol-cek-table">
+    <title>Column Encryption Key Algorithms</title>
+    <tgroup cols="7">
+     <thead>
+      <row>
+       <entry>PostgreSQL ID</entry>
+       <entry>Name</entry>
+       <entry>Description</entry>
+       <entry>MAC key length (octets)</entry>
+       <entry>Encryption key length (octets)</entry>
+       <entry>IV key length (octets)</entry>
+       <entry>Total key length (octets)</entry>
+      </row>
+     </thead>
+     <tbody>
+      <row>
+       <entry>32768</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>16</entry>
+       <entry>16</entry>
+       <entry>16</entry>
+       <entry>48</entry>
+      </row>
+      <row>
+       <entry>32769</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>24</entry>
+       <entry>24</entry>
+       <entry>24</entry>
+       <entry>72</entry>
+      </row>
+      <row>
+       <entry>32770</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>24</entry>
+       <entry>32</entry>
+       <entry>24</entry>
+       <entry>90</entry>
+      </row>
+      <row>
+       <entry>32771</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>32</entry>
+       <entry>32</entry>
+       <entry>32</entry>
+       <entry>96</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 version number as a 16-bit
+    unsigned integer in network byte order.  The version number is currently
+    always 1.  (This is intended to allow for possible incompatible changes or
+    extensions in the future.)
+   </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 IV key, and
+    <replaceable>P</replaceable> is the plaintext to be encrypted.
+   </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 f5be638867a..bf598832d16 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 00000000000..655e1e00d8d
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_encryption_key.sgml
@@ -0,0 +1,197 @@
+<!--
+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 }
+ALTER COLUMN ENCRYPTION KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</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
+   schema.  (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 (optionally schema-qualified) 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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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 00000000000..7f0e656ef06
--- /dev/null
+++ b/doc/src/sgml/ref/alter_column_master_key.sgml
@@ -0,0 +1,134 @@
+<!--
+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> ( REALM = <replaceable>realm</replaceable> )
+
+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 }
+ALTER COLUMN MASTER KEY <replaceable>name</replaceable> SET SCHEMA <replaceable>new_schema</replaceable>
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+  <title>Description</title>
+
+  <para>
+   <command>ALTER COLUMN MASTER KEY</command> changes the definition of a
+   column master key.
+  </para>
+
+  <para>
+   The first form changes the parameters of a column master key.  See <xref
+   linkend="sql-create-column-master-key"/> for details.
+  </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 schema.
+   (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>
+
+   <varlistentry>
+    <term><replaceable class="parameter">new_schema</replaceable></term>
+    <listitem>
+     <para>
+      The new schema for 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/comment.sgml b/doc/src/sgml/ref/comment.sgml
index 5b43c56b133..1caf9bfa569 100644
--- a/doc/src/sgml/ref/comment.sgml
+++ b/doc/src/sgml/ref/comment.sgml
@@ -28,6 +28,8 @@
   CAST (<replaceable>source_type</replaceable> AS <replaceable>target_type</replaceable>) |
   COLLATION <replaceable class="parameter">object_name</replaceable> |
   COLUMN <replaceable class="parameter">relation_name</replaceable>.<replaceable class="parameter">column_name</replaceable> |
+  COLUMN ENCRYPTION KEY <replaceable class="parameter">object_name</replaceable> |
+  COLUMN MASTER KEY <replaceable class="parameter">object_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON <replaceable class="parameter">table_name</replaceable> |
   CONSTRAINT <replaceable class="parameter">constraint_name</replaceable> ON DOMAIN <replaceable class="parameter">domain_name</replaceable> |
   CONVERSION <replaceable class="parameter">object_name</replaceable> |
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index 33ce7c4ea6c..213c8e8ed20 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -625,6 +625,16 @@ <title>Notes</title>
     null strings to null values and unquoted null strings to empty strings.
    </para>
 
+   <para>
+    <command>COPY</command> does not support automatic client-side
+    column-level encryption or decryption; its input or output data will
+    always be the ciphertext.  This is usually suitable for backups (see also
+    <xref linkend="app-pgdump"/>).  If automatic client-side encryption or
+    decryption is wanted, <command>INSERT</command> and
+    <command>SELECT</command> need to be used instead to write and read the
+    data.
+   </para>
+
  </refsect1>
 
  <refsect1>
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 00000000000..65534fb03f3
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_encryption_key.sgml
@@ -0,0 +1,173 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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.  You must have <literal>USAGE</literal> privilege on the
+      column master 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>unspecified</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_1</literal></para>
+       </listitem>
+       <listitem>
+        <para><literal>RSAES_OAEP_SHA_256</literal></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.  In that
+      case, specifying the algorithm as <literal>unspecified</literal> would be
+      appropriate.
+     </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,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    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,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    ENCRYPTED_VALUE = '\x01020204...'
+),
+(
+    COLUMN_MASTER_KEY = cmk2,
+    ALGORITHM = 'RSAES_OAEP_SHA_1',
+    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 00000000000..6aaa1088d19
--- /dev/null
+++ b/doc/src/sgml/ref/create_column_master_key.sgml
@@ -0,0 +1,107 @@
+<!--
+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.  The name can be
+      schema-qualified.
+     </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 02f31d2d6fd..e9b301ca2e2 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 | DEFAULT } ] [ 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 | DEFAULT } ] [ 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> ... ] }
     [, ... ]
@@ -88,7 +88,7 @@
 
 <phrase>and <replaceable class="parameter">like_option</replaceable> is:</phrase>
 
-{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
+{ INCLUDING | EXCLUDING } { COMMENTS | COMPRESSION | CONSTRAINTS | DEFAULTS | ENCRYPTED | GENERATED | IDENTITY | INDEXES | STATISTICS | STORAGE | ALL }
 
 <phrase>and <replaceable class="parameter">partition_bound_spec</replaceable> is:</phrase>
 
@@ -352,6 +352,47 @@ <title>Parameters</title>
     </listitem>
    </varlistentry>
 
+   <varlistentry id="sql-createtable-parms-encrypted">
+    <term><literal>ENCRYPTED WITH ( <replaceable>encryption_options</replaceable> )</literal></term>
+    <listitem>
+     <para>
+      Enables automatic client-side column-level 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.  You must have <literal>USAGE</literal> privilege
+          on the column encryption key.
+         </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 id="sql-createtable-parms-inherits">
     <term><literal>INHERITS ( <replaceable>parent_table</replaceable> [, ... ] )</literal></term>
     <listitem>
@@ -717,6 +758,16 @@ <title>Parameters</title>
         </listitem>
        </varlistentry>
 
+       <varlistentry id="sql-createtable-parms-like-opt-encrypted">
+        <term><literal>INCLUDING ENCRYPTED</literal></term>
+        <listitem>
+         <para>
+          Column encryption specifications for the copied column definitions
+          will be copied.  By default, new columns will be unencrypted.
+         </para>
+        </listitem>
+       </varlistentry>
+
        <varlistentry id="sql-createtable-parms-like-opt-generated">
         <term><literal>INCLUDING GENERATED</literal></term>
         <listitem>
diff --git a/doc/src/sgml/ref/discard.sgml b/doc/src/sgml/ref/discard.sgml
index bf44c523cac..6a94706ef79 100644
--- a/doc/src/sgml/ref/discard.sgml
+++ b/doc/src/sgml/ref/discard.sgml
@@ -21,7 +21,7 @@
 
  <refsynopsisdiv>
 <synopsis>
-DISCARD { ALL | PLANS | SEQUENCES | TEMPORARY | TEMP }
+DISCARD { ALL | COLUMN ENCRYPTION KEYS | PLANS | SEQUENCES | TEMPORARY | TEMP }
 </synopsis>
  </refsynopsisdiv>
 
@@ -42,6 +42,17 @@ <title>Parameters</title>
 
   <variablelist>
 
+   <varlistentry>
+    <term><literal>COLUMN ENCRYPTION KEYS</literal></term>
+    <listitem>
+     <para>
+      Discards knowledge about which column encryption keys and column master
+      keys have been sent to the client in this session.  (They will
+      subsequently be re-sent as required.)
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>PLANS</literal></term>
     <listitem>
@@ -93,6 +104,7 @@ <title>Parameters</title>
 DISCARD PLANS;
 DISCARD TEMP;
 DISCARD SEQUENCES;
+DISCARD COLUMN ENCRYPTION KEYS;
 </programlisting></para>
     </listitem>
    </varlistentry>
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 00000000000..f2ac1beb084
--- /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 (optionally schema-qualified) 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 00000000000..fae95e09d19
--- /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 (optionally schema-qualified) 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/grant.sgml b/doc/src/sgml/ref/grant.sgml
index 65b1fe77119..480b5bda676 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -46,6 +46,16 @@
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
     [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
 
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN ENCRYPTION KEY <replaceable>cek_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
+GRANT { USAGE | ALL [ PRIVILEGES ] }
+    ON COLUMN MASTER KEY <replaceable>cmk_name</replaceable> [, ...]
+    TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
+    [ GRANTED BY <replaceable class="parameter">role_specification</replaceable> ]
+
 GRANT { USAGE | ALL [ PRIVILEGES ] }
     ON DOMAIN <replaceable>domain_name</replaceable> [, ...]
     TO <replaceable class="parameter">role_specification</replaceable> [, ...] [ WITH GRANT OPTION ]
@@ -518,7 +528,7 @@ <title>Compatibility</title>
    </para>
 
    <para>
-    Privileges on databases, tablespaces, schemas, languages, and
+    Privileges on databases, tablespaces, schemas, keys, languages, and
     configuration parameters are
     <productname>PostgreSQL</productname> extensions.
    </para>
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index b99793e4148..1b40fc66d47 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -744,6 +744,48 @@ <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 turns on the column encryption connection option in
+        <application>libpq</application> (see <xref
+        linkend="libpq-connect-column-encryption"/>).  Column master key
+        lookup must be configured by the user, either through a connection
+        option or an environment setting (see <xref
+        linkend="libpq-connect-cmklookup"/>).
+       </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.  (But then it is recommended
+        to not do this on the same host as the server, to avoid exposing
+        unencrypted data that is meant to be kept encrypted on the server.)
+        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 ... \bind ... \g 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 4d7c0464687..8e848c337a3 100644
--- a/doc/src/sgml/ref/pg_dumpall.sgml
+++ b/doc/src/sgml/ref/pg_dumpall.sgml
@@ -293,6 +293,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 539748ffc29..e56b19024fb 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1424,6 +1424,34 @@ <title>Meta-Commands</title>
       </varlistentry>
 
 
+      <varlistentry id="app-psql-meta-command-dcek">
+        <term><literal>\dcek[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column encryption keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        encryption keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
+      <varlistentry id="app-psql-meta-command-dcmk">
+        <term><literal>\dcmk[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
+        <listitem>
+        <para>
+        Lists column master keys.  If <replaceable
+        class="parameter">pattern</replaceable> is specified, only column
+        master keys whose names match the pattern are listed.  If
+        <literal>+</literal> is appended to the command name, each object is
+        listed with its associated description.
+        </para>
+        </listitem>
+      </varlistentry>
+
+
       <varlistentry id="app-psql-meta-command-dconfig">
         <term><literal>\dconfig[+] [ <link linkend="app-psql-patterns"><replaceable class="parameter">pattern</replaceable></link> ]</literal></term>
         <listitem>
@@ -4053,6 +4081,17 @@ <title>Variables</title>
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-hide-column-encryption">
+        <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 id="app-psql-variables-hide-toast-compression">
         <term><varname>HIDE_TOAST_COMPRESSION</varname></term>
         <listitem>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index ff85ace83fc..4e40bc41434 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 4c5eedcdc17..8a4a18b7ff5 100644
--- a/src/backend/access/common/printsimple.c
+++ b/src/backend/access/common/printsimple.c
@@ -20,8 +20,10 @@
 
 #include "access/printsimple.h"
 #include "catalog/pg_type.h"
+#include "libpq/libpq-be.h"
 #include "libpq/pqformat.h"
 #include "libpq/protocol.h"
+#include "miscadmin.h"
 #include "utils/builtins.h"
 
 /*
@@ -47,6 +49,12 @@ 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_sendint32(&buf, 0);	/* CEK alg */
+			pq_sendint16(&buf, 0);	/* flags */
+		}
 	}
 
 	pq_endmessage(&buf);
diff --git a/src/backend/access/common/printtup.c b/src/backend/access/common/printtup.c
index f2d5ca14fee..f9b788e64a1 100644
--- a/src/backend/access/common/printtup.c
+++ b/src/backend/access/common/printtup.c
@@ -15,12 +15,27 @@
  */
 #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-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,
@@ -150,6 +165,164 @@ 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;
+	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 = SysCacheGetAttrNotNull(CMKOID, tuple, Anum_pg_colmasterkey_cmkrealm);
+	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;
+
+	/*
+	 * We really only need data from pg_colenckeydata, but before we scan
+	 * that, let's check that an entry exists in pg_colenckey, so that if
+	 * there are catalog inconsistencies, we can locate them better.
+	 */
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(attcek)))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	/*
+	 * Now scan pg_colenckeydata.
+	 */
+	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_sendint32(&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;
+	}
+
+	/*
+	 * This is a user-facing message, because with ALTER it is possible to
+	 * delete all data entries for a CEK.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("no data for column encryption key \"%s\"", get_cek_name(attcek, false)));
+
+	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);
+}
+
+void
+DiscardColumnEncryptionKeys(void)
+{
+	list_free(cmk_sent);
+	cmk_sent = NIL;
+
+	list_free(cek_sent);
+	cek_sent = NIL;
+}
+
 /*
  * SendRowDescriptionMessage --- send a RowDescription message to the frontend
  *
@@ -166,6 +339,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);
 
@@ -182,14 +356,18 @@ 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(int32)	/* attencalg */
+			   + sizeof(int16));	/* flags */
+	enlargeStringInfo(buf, sz * natts);
 
 	for (i = 0; i < natts; ++i)
 	{
@@ -199,6 +377,9 @@ SendRowDescriptionMessage(StringInfo buf, TupleDesc typeinfo,
 		Oid			resorigtbl;
 		AttrNumber	resorigcol;
 		int16		format;
+		Oid			attcekid = InvalidOid;
+		int32		attencalg = 0;
+		int16		flags = 0;
 
 		/*
 		 * If column is a domain, send the base type and typmod instead.
@@ -230,6 +411,32 @@ 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);
+			attcekid = DatumGetObjectId(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attcek));
+			atttypid = DatumGetObjectId(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attusertypid));
+			atttypmod = DatumGetInt32(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attusertypmod));
+			attencalg = orig_att->atttypmod;
+			if (orig_att->atttypid == PG_ENCRYPTED_DETOID)
+				flags |= 0x0001;
+			ReleaseSysCache(tp);
+
+			MaybeSendColumnEncryptionKeyMessage(attcekid);
+
+			/*
+			 * Encrypted types are always sent in binary when column
+			 * encryption is enabled.
+			 */
+			format = 1;
+		}
+
 		pq_writestring(buf, NameStr(att->attname));
 		pq_writeint32(buf, resorigtbl);
 		pq_writeint16(buf, resorigcol);
@@ -237,6 +444,12 @@ 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_writeint32(buf, attencalg);
+			pq_writeint16(buf, flags);
+		}
 	}
 
 	pq_endmessage_reuse(buf);
@@ -270,6 +483,13 @@ printtup_prepare_info(DR_printtup *myState, TupleDesc typeinfo, int numAttrs)
 		int16		format = (formats ? formats[i] : 0);
 		Form_pg_attribute attr = TupleDescAttr(typeinfo, i);
 
+		/*
+		 * Encrypted types are always sent in binary when column encryption is
+		 * enabled.
+		 */
+		if (MyProcPort->column_encryption_enabled && type_is_encrypted(attr->atttypid))
+			format = 1;
+
 		thisState->format = format;
 		if (format == 0)
 		{
diff --git a/src/backend/access/hash/hashvalidate.c b/src/backend/access/hash/hashvalidate.c
index 40164e2ea2b..a6646748706 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/bootstrap/bootparse.y b/src/backend/bootstrap/bootparse.y
index 3c9c1da0216..438ed6e1cfe 100644
--- a/src/backend/bootstrap/bootparse.y
+++ b/src/backend/bootstrap/bootparse.y
@@ -226,6 +226,7 @@ Boot_CreateStmt:
 													  BOOTSTRAP_SUPERUSERID,
 													  HEAP_TABLE_AM_OID,
 													  tupdesc,
+													  NULL,
 													  NIL,
 													  RELKIND_RELATION,
 													  RELPERSISTENCE_PERMANENT,
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 7abf3c2a74a..c4fa1b48e9b 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -51,6 +51,8 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_default_acl.h"
 #include "catalog/pg_foreign_data_wrapper.h"
@@ -256,6 +258,12 @@ restrict_and_check_grant(bool is_grant, AclMode avail_goptions, bool all_privs,
 		case OBJECT_SEQUENCE:
 			whole_mask = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			whole_mask = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			whole_mask = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			whole_mask = ACL_ALL_RIGHTS_DATABASE;
 			break;
@@ -482,6 +490,14 @@ ExecuteGrantStmt(GrantStmt *stmt)
 			all_privileges = ACL_ALL_RIGHTS_SEQUENCE;
 			errormsg = gettext_noop("invalid privilege type %s for sequence");
 			break;
+		case OBJECT_CEK:
+			all_privileges = ACL_ALL_RIGHTS_CEK;
+			errormsg = gettext_noop("invalid privilege type %s for column encryption key");
+			break;
+		case OBJECT_CMK:
+			all_privileges = ACL_ALL_RIGHTS_CMK;
+			errormsg = gettext_noop("invalid privilege type %s for column master key");
+			break;
 		case OBJECT_DATABASE:
 			all_privileges = ACL_ALL_RIGHTS_DATABASE;
 			errormsg = gettext_noop("invalid privilege type %s for database");
@@ -606,6 +622,12 @@ ExecGrantStmt_oids(InternalGrant *istmt)
 		case OBJECT_SEQUENCE:
 			ExecGrant_Relation(istmt);
 			break;
+		case OBJECT_CEK:
+			ExecGrant_common(istmt, ColumnEncKeyRelationId, ACL_ALL_RIGHTS_CEK, NULL);
+			break;
+		case OBJECT_CMK:
+			ExecGrant_common(istmt, ColumnMasterKeyRelationId, ACL_ALL_RIGHTS_CMK, NULL);
+			break;
 		case OBJECT_DATABASE:
 			ExecGrant_common(istmt, DatabaseRelationId, ACL_ALL_RIGHTS_DATABASE, NULL);
 			break;
@@ -685,6 +707,26 @@ objectNamesToOids(ObjectType objtype, List *objnames, bool is_grant)
 				objects = lappend_oid(objects, relOid);
 			}
 			break;
+		case OBJECT_CEK:
+			foreach(cell, objnames)
+			{
+				List	   *cekname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cek_oid(cekname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
+		case OBJECT_CMK:
+			foreach(cell, objnames)
+			{
+				List	   *cmkname = (List *) lfirst(cell);
+				Oid			oid;
+
+				oid = get_cmk_oid(cmkname, false);
+				objects = lappend_oid(objects, oid);
+			}
+			break;
 		case OBJECT_DATABASE:
 			foreach(cell, objnames)
 			{
@@ -2702,6 +2744,12 @@ aclcheck_error(AclResult aclerr, ObjectType objtype,
 					case OBJECT_AGGREGATE:
 						msg = gettext_noop("permission denied for aggregate %s");
 						break;
+					case OBJECT_CEK:
+						msg = gettext_noop("permission denied for column encryption key %s");
+						break;
+					case OBJECT_CMK:
+						msg = gettext_noop("permission denied for column master key %s");
+						break;
 					case OBJECT_COLLATION:
 						msg = gettext_noop("permission denied for collation %s");
 						break;
@@ -2807,6 +2855,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:
@@ -2837,6 +2886,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;
@@ -2947,6 +3002,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:
@@ -3028,6 +3084,10 @@ pg_aclmask(ObjectType objtype, Oid object_oid, AttrNumber attnum, Oid roleid,
 		case OBJECT_TABLE:
 		case OBJECT_SEQUENCE:
 			return pg_class_aclmask(object_oid, roleid, mask, how);
+		case OBJECT_CEK:
+			return object_aclmask(ColumnEncKeyRelationId, object_oid, roleid, mask, how);
+		case OBJECT_CMK:
+			return object_aclmask(ColumnMasterKeyRelationId, object_oid, roleid, mask, how);
 		case OBJECT_DATABASE:
 			return object_aclmask(DatabaseRelationId, object_oid, roleid, mask, how);
 		case OBJECT_FUNCTION:
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index 6e8f6a57051..ed2a01a08da 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"
@@ -1444,6 +1447,9 @@ doDeletion(const ObjectAddress *object, int flags)
 
 		case CastRelationId:
 		case CollationRelationId:
+		case ColumnEncKeyRelationId:
+		case ColumnEncKeyDataRelationId:
+		case ColumnMasterKeyRelationId:
 		case ConversionRelationId:
 		case LanguageRelationId:
 		case OperatorClassRelationId:
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index cc31909012d..bc68fc9bf3d 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -42,6 +42,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_foreign_table.h"
@@ -453,7 +454,7 @@ heap_create(const char *relname,
  * --------------------------------
  */
 void
-CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
+CheckAttributeNamesTypes(TupleDesc tupdesc, const FormExtraData_pg_attribute tupdesc_extra[], char relkind,
 						 int flags)
 {
 	int			i;
@@ -512,7 +513,14 @@ CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
 						   TupleDescAttr(tupdesc, i)->atttypid,
 						   TupleDescAttr(tupdesc, i)->attcollation,
 						   NIL, /* assume we're creating a new rowtype */
-						   flags);
+						   flags |
+
+		/*
+		 * Allow encrypted types if CEK has been provided, which means this
+		 * type has been internally generated.  We don't want to allow
+		 * explicitly using these types.
+		 */
+						   (tupdesc_extra && !tupdesc_extra[i].attcek.isnull ? CHKATYPE_ENCRYPTED : 0));
 	}
 }
 
@@ -657,6 +665,21 @@ CheckAttributeType(const char *attname,
 						   flags);
 	}
 
+	/*
+	 * Encrypted types are not allowed explictly as column types.  Most
+	 * callers run this check before transforming the column definition to use
+	 * the encrypted types.  Some callers call it again after; those should
+	 * set the CHKATYPE_ENCRYPTED to let this pass.
+	 */
+	if (type_is_encrypted(atttypid) && !(flags & CHKATYPE_ENCRYPTED))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+				 errbacktrace(),
+				 errmsg("column \"%s\" has internal type %s",
+						attname, format_type_be(atttypid))));
+	}
+
 	/*
 	 * This might not be strictly invalid per SQL standard, but it is pretty
 	 * useless, and it cannot be dumped, so we must disallow it.
@@ -763,11 +786,23 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 
 			slot[slotCount]->tts_values[Anum_pg_attribute_attoptions - 1] = attrs_extra->attoptions.value;
 			slot[slotCount]->tts_isnull[Anum_pg_attribute_attoptions - 1] = attrs_extra->attoptions.isnull;
+
+			slot[slotCount]->tts_values[Anum_pg_attribute_attcek - 1] = attrs_extra->attcek.value;
+			slot[slotCount]->tts_isnull[Anum_pg_attribute_attcek - 1] = attrs_extra->attcek.isnull;
+
+			slot[slotCount]->tts_values[Anum_pg_attribute_attusertypid - 1] = attrs_extra->attusertypid.value;
+			slot[slotCount]->tts_isnull[Anum_pg_attribute_attusertypid - 1] = attrs_extra->attusertypid.isnull;
+
+			slot[slotCount]->tts_values[Anum_pg_attribute_attusertypmod - 1] = attrs_extra->attusertypmod.value;
+			slot[slotCount]->tts_isnull[Anum_pg_attribute_attusertypmod - 1] = attrs_extra->attusertypmod.isnull;
 		}
 		else
 		{
 			slot[slotCount]->tts_isnull[Anum_pg_attribute_attstattarget - 1] = true;
 			slot[slotCount]->tts_isnull[Anum_pg_attribute_attoptions - 1] = true;
+			slot[slotCount]->tts_isnull[Anum_pg_attribute_attcek - 1] = true;
+			slot[slotCount]->tts_isnull[Anum_pg_attribute_attusertypid - 1] = true;
+			slot[slotCount]->tts_isnull[Anum_pg_attribute_attusertypmod - 1] = true;
 		}
 
 		/*
@@ -819,6 +854,7 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 static void
 AddNewAttributeTuples(Oid new_rel_oid,
 					  TupleDesc tupdesc,
+					  const FormExtraData_pg_attribute tupdesc_extra[],
 					  char relkind)
 {
 	Relation	rel;
@@ -834,7 +870,7 @@ AddNewAttributeTuples(Oid new_rel_oid,
 
 	indstate = CatalogOpenIndexes(rel);
 
-	InsertPgAttributeTuples(rel, tupdesc, new_rel_oid, NULL, indstate);
+	InsertPgAttributeTuples(rel, tupdesc, new_rel_oid, tupdesc_extra, indstate);
 
 	/* add dependencies on their datatypes and collations */
 	for (int i = 0; i < natts; i++)
@@ -853,6 +889,20 @@ AddNewAttributeTuples(Oid new_rel_oid,
 							 tupdesc->attrs[i].attcollation);
 			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
 		}
+
+		if (tupdesc_extra && !tupdesc_extra[i].attcek.isnull)
+		{
+			ObjectAddressSet(referenced, ColumnEncKeyRelationId,
+							 DatumGetObjectId(tupdesc_extra[i].attcek.value));
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
+
+		if (tupdesc_extra && !tupdesc_extra[i].attusertypid.isnull)
+		{
+			ObjectAddressSet(referenced, TypeRelationId,
+							 DatumGetObjectId(tupdesc_extra[i].attusertypid.value));
+			recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+		}
 	}
 
 	/*
@@ -1110,6 +1160,7 @@ heap_create_with_catalog(const char *relname,
 						 Oid ownerid,
 						 Oid accessmtd,
 						 TupleDesc tupdesc,
+						 const FormExtraData_pg_attribute tupdesc_extra[],
 						 List *cooked_constraints,
 						 char relkind,
 						 char relpersistence,
@@ -1147,7 +1198,7 @@ heap_create_with_catalog(const char *relname,
 	 * allow_system_table_mods is on, allow ANYARRAY to be used; this is a
 	 * hack to allow creating pg_statistic and cloning it during VACUUM FULL.
 	 */
-	CheckAttributeNamesTypes(tupdesc, relkind,
+	CheckAttributeNamesTypes(tupdesc, tupdesc_extra, relkind,
 							 allow_system_table_mods ? CHKATYPE_ANYARRAY : 0);
 
 	/*
@@ -1414,7 +1465,7 @@ heap_create_with_catalog(const char *relname,
 	/*
 	 * now add tuples to pg_attribute for the attributes in our new relation.
 	 */
-	AddNewAttributeTuples(relid, new_rel_desc->rd_att, relkind);
+	AddNewAttributeTuples(relid, new_rel_desc->rd_att, tupdesc_extra, relkind);
 
 	/*
 	 * Make a dependency link to force the relation to be deleted if its
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 9b7ef71d6fe..9a03328e5d1 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -529,6 +529,10 @@ AppendAttributeTuples(Relation indexRelation, const Datum *attopts, const Nullab
 				attrs_extra[i].attstattarget = stattargets[i];
 			else
 				attrs_extra[i].attstattarget.isnull = true;
+
+			attrs_extra[i].attcek.isnull = true;
+			attrs_extra[i].attusertypid.isnull = true;
+			attrs_extra[i].attusertypmod.isnull = true;
 		}
 	}
 
diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c
index 4548a917234..81018f74f7a 100644
--- a/src/backend/catalog/namespace.c
+++ b/src/backend/catalog/namespace.c
@@ -27,6 +27,8 @@
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_authid.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_conversion.h"
 #include "catalog/pg_database.h"
@@ -2298,6 +2300,254 @@ OpfamilyIsVisibleExt(Oid opfid, bool *is_missing)
 	return visible;
 }
 
+/*
+ * get_cek_oid - find a CEK by possibly qualified name
+ */
+Oid
+get_cek_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cekname;
+	Oid			namespaceId;
+	Oid			cekoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cekname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cekoid = InvalidOid;
+		else
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cekoid = GetSysCacheOid2(CEKNAMENSP, Anum_pg_colenckey_oid,
+									 PointerGetDatum(cekname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cekoid))
+				return cekoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cekoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column encryption key \"%s\" does not exist",
+						NameListToString(names))));
+	return cekoid;
+}
+
+/*
+ * CEKIsVisible
+ *		Determine whether a CEK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified CEK name".
+ */
+bool
+CEKIsVisible(Oid cekid)
+{
+	HeapTuple	tup;
+	Form_pg_colenckey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CEKOID, ObjectIdGetDatum(cekid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", cekid);
+	form = (Form_pg_colenckey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->ceknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CEKs.
+		 */
+		char	   *name = NameStr(form->cekname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CEKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
+/*
+ * get_cmk_oid - find a CMK by possibly qualified name
+ */
+Oid
+get_cmk_oid(List *names, bool missing_ok)
+{
+	char	   *schemaname;
+	char	   *cmkname;
+	Oid			namespaceId;
+	Oid			cmkoid = InvalidOid;
+	ListCell   *l;
+
+	/* deconstruct the name list */
+	DeconstructQualifiedName(names, &schemaname, &cmkname);
+
+	if (schemaname)
+	{
+		/* use exact schema given */
+		namespaceId = LookupExplicitNamespace(schemaname, missing_ok);
+		if (missing_ok && !OidIsValid(namespaceId))
+			cmkoid = InvalidOid;
+		else
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+	}
+	else
+	{
+		/* search for it in search path */
+		recomputeNamespacePath();
+
+		foreach(l, activeSearchPath)
+		{
+			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			cmkoid = GetSysCacheOid2(CMKNAMENSP, Anum_pg_colmasterkey_oid,
+									 PointerGetDatum(cmkname),
+									 ObjectIdGetDatum(namespaceId));
+			if (OidIsValid(cmkoid))
+				return cmkoid;
+		}
+	}
+
+	/* Not found in path */
+	if (!OidIsValid(cmkoid) && !missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("column master key \"%s\" does not exist",
+						NameListToString(names))));
+	return cmkoid;
+}
+
+/*
+ * CMKIsVisible
+ *		Determine whether a CMK (identified by OID) is visible in the
+ *		current search path.  Visible means "would be found by searching
+ *		for the unqualified CMK name".
+ */
+bool
+CMKIsVisible(Oid cmkid)
+{
+	HeapTuple	tup;
+	Form_pg_colmasterkey form;
+	Oid			namespace;
+	bool		visible;
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkid));
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkid);
+	form = (Form_pg_colmasterkey) GETSTRUCT(tup);
+
+	recomputeNamespacePath();
+
+	/*
+	 * Quick check: if it ain't in the path at all, it ain't visible. Items in
+	 * the system namespace are surely in the path and so we needn't even do
+	 * list_member_oid() for them.
+	 */
+	namespace = form->cmknamespace;
+	if (namespace != PG_CATALOG_NAMESPACE &&
+		!list_member_oid(activeSearchPath, namespace))
+		visible = false;
+	else
+	{
+		/*
+		 * If it is in the path, it might still not be visible; it could be
+		 * hidden by another parser of the same name earlier in the path. So
+		 * we must do a slow check for conflicting CMKs.
+		 */
+		char	   *name = NameStr(form->cmkname);
+		ListCell   *l;
+
+		visible = false;
+		foreach(l, activeSearchPath)
+		{
+			Oid			namespaceId = lfirst_oid(l);
+
+			if (namespaceId == myTempNamespace)
+				continue;		/* do not look in temp namespace */
+
+			if (namespaceId == namespace)
+			{
+				/* Found it first in path */
+				visible = true;
+				break;
+			}
+			if (SearchSysCacheExists2(CMKNAMENSP,
+									  PointerGetDatum(name),
+									  ObjectIdGetDatum(namespaceId)))
+			{
+				/* Found something else first in path */
+				break;
+			}
+		}
+	}
+
+	ReleaseSysCache(tup);
+
+	return visible;
+}
+
 /*
  * lookup_collation
  *		If there's a collation of the given name/namespace, and it works
@@ -4950,6 +5200,28 @@ pg_opfamily_is_visible(PG_FUNCTION_ARGS)
 	PG_RETURN_BOOL(result);
 }
 
+Datum
+pg_cek_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CEKIsVisible(oid));
+}
+
+Datum
+pg_cmk_is_visible(PG_FUNCTION_ARGS)
+{
+	Oid			oid = PG_GETARG_OID(0);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(oid)))
+		PG_RETURN_NULL();
+
+	PG_RETURN_BOOL(CMKIsVisible(oid));
+}
+
 Datum
 pg_collation_is_visible(PG_FUNCTION_ARGS)
 {
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 7b536ac6fde..dc3baa304cc 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -28,7 +28,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"
@@ -188,6 +191,48 @@ static const ObjectPropertyType ObjectProperty[] =
 		OBJECT_COLLATION,
 		true
 	},
+	{
+		"column encryption key",
+		ColumnEncKeyRelationId,
+		ColumnEncKeyOidIndexId,
+		CEKOID,
+		CEKNAMENSP,
+		Anum_pg_colenckey_oid,
+		Anum_pg_colenckey_cekname,
+		Anum_pg_colenckey_ceknamespace,
+		Anum_pg_colenckey_cekowner,
+		Anum_pg_colenckey_cekacl,
+		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,
+		CMKNAMENSP,
+		Anum_pg_colmasterkey_oid,
+		Anum_pg_colmasterkey_cmkname,
+		Anum_pg_colmasterkey_cmknamespace,
+		Anum_pg_colmasterkey_cmkowner,
+		Anum_pg_colmasterkey_cmkacl,
+		OBJECT_CMK,
+		true
+	},
 	{
 		"constraint",
 		ConstraintRelationId,
@@ -721,6 +766,15 @@ static const struct object_type_map
 	{
 		"collation", OBJECT_COLLATION
 	},
+	{
+		"column encryption key", OBJECT_CEK
+	},
+	{
+		"column encryption key data", OBJECT_CEKDATA
+	},
+	{
+		"column master key", OBJECT_CMK
+	},
 	{
 		"table constraint", OBJECT_TABCONSTRAINT
 	},
@@ -991,6 +1045,16 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEK:
+				address.classId = ColumnEncKeyRelationId;
+				address.objectId = get_cek_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
+			case OBJECT_CMK:
+				address.classId = ColumnMasterKeyRelationId;
+				address.objectId = get_cmk_oid(castNode(List, object), missing_ok);
+				address.objectSubId = 0;
+				break;
 			case OBJECT_DATABASE:
 			case OBJECT_EXTENSION:
 			case OBJECT_TABLESPACE:
@@ -1070,6 +1134,21 @@ get_object_address(ObjectType objtype, Node *object,
 					address.objectSubId = 0;
 				}
 				break;
+			case OBJECT_CEKDATA:
+				{
+					List	   *cekname = linitial_node(List, castNode(List, object));
+					List	   *cmkname = lsecond_node(List, 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));
@@ -2273,6 +2352,8 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 		case OBJECT_FOREIGN_TABLE:
 		case OBJECT_COLUMN:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_STATISTIC_EXT:
@@ -2329,6 +2410,7 @@ pg_get_object_address(PG_FUNCTION_ARGS)
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 			objnode = (Node *) list_make2(name, args);
 			break;
 		case OBJECT_FUNCTION:
@@ -2451,6 +2533,8 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 				aclcheck_error(ACLCHECK_NOT_OWNER, objtype,
 							   strVal(object));
 			break;
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_OPCLASS:
@@ -2543,6 +2627,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address,
 			break;
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_PUBLICATION_NAMESPACE:
@@ -3005,6 +3090,94 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
 				break;
 			}
 
+		case ColumnEncKeyRelationId:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *nspname;
+
+				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);
+
+				/* Qualify the name if not visible in search path */
+				if (CEKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->ceknamespace);
+
+				appendStringInfo(&buffer, _("column encryption key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case ColumnEncKeyDataRelationId:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckeydata cekdata;
+				ObjectAddress cekaddr, cmkaddr;
+
+				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);
+
+				ObjectAddressSet(cekaddr, ColumnEncKeyRelationId, cekdata->ckdcekid);
+				ObjectAddressSet(cmkaddr, ColumnMasterKeyRelationId, cekdata->ckdcmkid);
+				appendStringInfo(&buffer, _("column encryption key data of %s for %s"),
+								 getObjectDescription(&cekaddr, false), getObjectDescription(&cmkaddr, false));
+
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case ColumnMasterKeyRelationId:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *nspname;
+
+				tup = SearchSysCache1(CMKOID,
+									  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_colmasterkey) GETSTRUCT(tup);
+
+				/* Qualify the name if not visible in search path */
+				if (CMKIsVisible(object->objectId))
+					nspname = NULL;
+				else
+					nspname = get_namespace_name(form->cmknamespace);
+
+				appendStringInfo(&buffer, _("column master key %s"),
+								 quote_qualified_identifier(nspname,
+															NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case ConstraintRelationId:
 			{
 				HeapTuple	conTup;
@@ -4400,6 +4573,18 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
 			appendStringInfoString(&buffer, "collation");
 			break;
 
+		case ColumnEncKeyRelationId:
+			appendStringInfoString(&buffer, "column encryption key");
+			break;
+
+		case ColumnEncKeyDataRelationId:
+			appendStringInfoString(&buffer, "column encryption key data");
+			break;
+
+		case ColumnMasterKeyRelationId:
+			appendStringInfoString(&buffer, "column master key");
+			break;
+
 		case ConstraintRelationId:
 			getConstraintTypeDescription(&buffer, object->objectId,
 										 missing_ok);
@@ -4863,6 +5048,108 @@ getObjectIdentityParts(const ObjectAddress *object,
 				break;
 			}
 
+		case ColumnEncKeyRelationId:
+			{
+				HeapTuple	tup;
+				Form_pg_colenckey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->ceknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cekname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cekname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case ColumnEncKeyDataRelationId:
+			{
+				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",
+								 getObjectIdentityParts(&(ObjectAddress){ColumnEncKeyRelationId, form->ckdcekid}, NULL, NULL, false),
+								 getObjectIdentityParts(&(ObjectAddress){ColumnMasterKeyRelationId, form->ckdcmkid}, NULL, NULL, false));
+
+				if (objname)
+				{
+					HeapTuple	tup2;
+					Form_pg_colenckey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CEKOID, ObjectIdGetDatum(form->ckdcekid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column encryption key %u", form->ckdcekid);
+					form2 = (Form_pg_colenckey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->ceknamespace);
+					*objname = list_make2(schema, pstrdup(NameStr(form2->cekname)));
+					ReleaseSysCache(tup2);
+				}
+				if (objargs)
+				{
+					HeapTuple	tup2;
+					Form_pg_colmasterkey form2;
+					char	   *schema;
+
+					tup2 = SearchSysCache1(CMKOID, ObjectIdGetDatum(form->ckdcmkid));
+					if (!HeapTupleIsValid(tup2))
+						elog(ERROR, "cache lookup failed for column master key %u", form->ckdcmkid);
+					form2 = (Form_pg_colmasterkey) GETSTRUCT(tup2);
+					schema = get_namespace_name_or_temp(form2->cmknamespace);
+					if (objargs)
+						*objargs = list_make2(schema, pstrdup(NameStr(form2->cmkname)));
+					ReleaseSysCache(tup2);
+				}
+				ReleaseSysCache(tup);
+				break;
+			}
+
+		case ColumnMasterKeyRelationId:
+			{
+				HeapTuple	tup;
+				Form_pg_colmasterkey form;
+				char	   *schema;
+
+				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);
+				schema = get_namespace_name_or_temp(form->cmknamespace);
+				appendStringInfoString(&buffer,
+									   quote_qualified_identifier(schema,
+																  NameStr(form->cmkname)));
+				if (objname)
+					*objname = list_make2(schema, pstrdup(NameStr(form->cmkname)));
+				ReleaseSysCache(tup);
+				break;
+			}
+
 		case ConstraintRelationId:
 			{
 				HeapTuple	conTup;
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 738bc46ae82..09f81e739fd 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -252,6 +252,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 										   rel->rd_rel->relowner,
 										   table_relation_toast_am(rel),
 										   tupdesc,
+										   NULL,
 										   NIL,
 										   RELKIND_TOASTVALUE,
 										   rel->rd_rel->relpersistence,
diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile
index cede90c3b98..cf7a533e0fe 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 12802b9d3fd..cfea5929972 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -21,7 +21,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_database_d.h"
 #include "catalog/pg_event_trigger.h"
@@ -115,6 +117,12 @@ report_namespace_conflict(Oid classId, const char *name, Oid nspOid)
 
 	switch (classId)
 	{
+		case ColumnEncKeyRelationId:
+			msgfmt = gettext_noop("column encryption key \"%s\" already exists in schema \"%s\"");
+			break;
+		case ColumnMasterKeyRelationId:
+			msgfmt = gettext_noop("column master key \"%s\" already exists in schema \"%s\"");
+			break;
 		case ConversionRelationId:
 			Assert(OidIsValid(nspOid));
 			msgfmt = gettext_noop("conversion \"%s\" already exists in schema \"%s\"");
@@ -400,6 +408,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:
@@ -548,6 +558,8 @@ ExecAlterObjectSchemaStmt(AlterObjectSchemaStmt *stmt,
 
 			/* generic code path */
 		case OBJECT_AGGREGATE:
+		case OBJECT_CEK:
+		case OBJECT_CMK:
 		case OBJECT_COLLATION:
 		case OBJECT_CONVERSION:
 		case OBJECT_FUNCTION:
@@ -861,6 +873,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/cluster.c b/src/backend/commands/cluster.c
index c04886c4090..2c6a31dd3be 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -747,6 +747,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 										  OldHeap->rd_rel->relowner,
 										  NewAccessMethod,
 										  OldHeapDesc,
+										  NULL,
 										  NIL,
 										  RELKIND_RELATION,
 										  relpersistence,
diff --git a/src/backend/commands/colenccmds.c b/src/backend/commands/colenccmds.c
new file mode 100644
index 00000000000..2a59aa4e1bf
--- /dev/null
+++ b/src/backend/commands/colenccmds.c
@@ -0,0 +1,449 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.c
+ *	  column-encryption-related commands support code
+ *
+ * Portions Copyright (c) 1996-2024, 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/namespace.h"
+#include "catalog/objectaccess.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colenckeydata.h"
+#include "catalog/pg_colmasterkey.h"
+#include "catalog/pg_namespace.h"
+#include "commands/colenccmds.h"
+#include "commands/dbcommands.h"
+#include "commands/defrem.h"
+#include "common/colenc.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, int *alg_p, char **encval_p)
+{
+	ListCell   *lc;
+	DefElem    *cmkEl = NULL;
+	DefElem    *algEl = NULL;
+	DefElem    *encvalEl = NULL;
+	Oid			cmkoid = InvalidOid;
+	int			alg = 0;
+	char	   *encval = NULL;
+
+	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)
+	{
+		List	   *val = defGetQualifiedName(cmkEl);
+
+		cmkoid = get_cmk_oid(val, false);
+	}
+	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")));
+
+		alg = get_cmkalg_num(val);
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val),
+					parser_errposition(pstate, algEl->location));
+	}
+	else
+	{
+		if (alg_p)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("attribute \"%s\" must be specified",
+							"algorithm")));
+	}
+
+	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, int 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] = Int32GetDatum(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)
+{
+	Oid			namespaceId;
+	char	   *ceknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	ObjectAddress referenced;
+	Oid			cekoid;
+	ListCell   *lc;
+	NameData	cekname;
+	Datum		values[Natts_pg_colenckey] = {0};
+	bool		nulls[Natts_pg_colenckey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &ceknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnEncKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CEKNAMENSP, PointerGetDatum(ceknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column encryption key \"%s\" already exists", ceknamestr));
+
+	cekoid = GetNewOidWithIndex(rel, ColumnEncKeyOidIndexId, Anum_pg_colenckey_oid);
+
+	foreach(lc, stmt->definition)
+	{
+		List	   *definition = lfirst_node(List, lc);
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+
+		parse_cek_attributes(pstate, definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		/* pg_colenckeydata */
+		insert_cekdata_record(cekoid, cmkoid, alg, encval);
+	}
+
+	/* pg_colenckey */
+	namestrcpy(&cekname, ceknamestr);
+	values[Anum_pg_colenckey_oid - 1] = ObjectIdGetDatum(cekoid);
+	values[Anum_pg_colenckey_cekname - 1] = NameGetDatum(&cekname);
+	values[Anum_pg_colenckey_ceknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colenckey_cekowner - 1] = ObjectIdGetDatum(GetUserId());
+	nulls[Anum_pg_colenckey_cekacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnEncKeyRelationId, cekoid);
+
+	ObjectAddressSet(referenced, NamespaceRelationId, namespaceId);
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+	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 (!object_ownercheck(ColumnEncKeyRelationId, cekoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CEK, NameListToString(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);
+	}
+	else
+	{
+		Oid			cmkoid = 0;
+		int			alg;
+		char	   *encval;
+		AclResult	aclresult;
+
+		parse_cek_attributes(pstate, stmt->definition, &cmkoid, &alg, &encval);
+
+		aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkoid, GetUserId(), ACL_USAGE);
+		if (aclresult != ACLCHECK_OK)
+			aclcheck_error(aclresult, OBJECT_CMK, get_cmk_name(cmkoid, false));
+
+		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\"",
+						   NameListToString(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)
+{
+	Oid			namespaceId;
+	char	   *cmknamestr;
+	AclResult	aclresult;
+	Relation	rel;
+	ObjectAddress myself;
+	ObjectAddress referenced;
+	Oid			cmkoid;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	char	   *realm;
+	NameData	cmkname;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	HeapTuple	tup;
+
+	namespaceId = QualifiedNameGetCreationNamespace(stmt->defnames, &cmknamestr);
+
+	aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(), ACL_CREATE);
+	if (aclresult != ACLCHECK_OK)
+		aclcheck_error(aclresult, OBJECT_SCHEMA, get_namespace_name(namespaceId));
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	if (SearchSysCacheExists2(CMKNAMENSP, PointerGetDatum(cmknamestr), ObjectIdGetDatum(namespaceId)))
+		ereport(ERROR,
+				errcode(ERRCODE_DUPLICATE_OBJECT),
+				errmsg("column master key \"%s\" already exists", cmknamestr));
+
+	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);
+	namestrcpy(&cmkname, cmknamestr);
+	values[Anum_pg_colmasterkey_oid - 1] = ObjectIdGetDatum(cmkoid);
+	values[Anum_pg_colmasterkey_cmkname - 1] = NameGetDatum(&cmkname);
+	values[Anum_pg_colmasterkey_cmknamespace - 1] = ObjectIdGetDatum(namespaceId);
+	values[Anum_pg_colmasterkey_cmkowner - 1] = ObjectIdGetDatum(GetUserId());
+	values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(realm);
+	nulls[Anum_pg_colmasterkey_cmkacl - 1] = true;
+
+	tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
+	CatalogTupleInsert(rel, tup);
+	heap_freetuple(tup);
+
+	ObjectAddressSet(myself, ColumnMasterKeyRelationId, cmkoid);
+
+	ObjectAddressSet(referenced, NamespaceRelationId, namespaceId);
+	recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
+
+	recordDependencyOnOwner(ColumnMasterKeyRelationId, cmkoid, GetUserId());
+
+	table_close(rel, RowExclusiveLock);
+
+	InvokeObjectPostCreateHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	return myself;
+}
+
+ObjectAddress
+AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt)
+{
+	Oid			cmkoid;
+	Relation	rel;
+	HeapTuple	tup;
+	HeapTuple	newtup;
+	ObjectAddress address;
+	ListCell   *lc;
+	DefElem    *realmEl = NULL;
+	Datum		values[Natts_pg_colmasterkey] = {0};
+	bool		nulls[Natts_pg_colmasterkey] = {0};
+	bool		replaces[Natts_pg_colmasterkey] = {0};
+
+	cmkoid = get_cmk_oid(stmt->cmkname, false);
+
+	rel = table_open(ColumnMasterKeyRelationId, RowExclusiveLock);
+
+	tup = SearchSysCache1(CMKOID, ObjectIdGetDatum(cmkoid));
+
+	if (!HeapTupleIsValid(tup))
+		elog(ERROR, "cache lookup failed for column master key %u", cmkoid);
+
+	if (!object_ownercheck(ColumnMasterKeyRelationId, cmkoid, GetUserId()))
+		aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_CMK, NameListToString(stmt->cmkname));
+
+	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)
+	{
+		values[Anum_pg_colmasterkey_cmkrealm - 1] = CStringGetTextDatum(defGetString(realmEl));
+		replaces[Anum_pg_colmasterkey_cmkrealm - 1] = true;
+	}
+
+	newtup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces);
+
+	CatalogTupleUpdate(rel, &tup->t_self, newtup);
+
+	InvokeObjectPostAlterHook(ColumnMasterKeyRelationId, cmkoid, 0);
+
+	ObjectAddressSet(address, ColumnMasterKeyRelationId, cmkoid);
+
+	heap_freetuple(newtup);
+	ReleaseSysCache(tup);
+
+	table_close(rel, RowExclusiveLock);
+
+	return address;
+}
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index c5df96e374a..9f11a26ff5f 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -45,6 +45,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/syscache.h"
 
 typedef struct
 {
@@ -211,6 +212,24 @@ create_ctas_nodata(List *tlist, IntoClause *into)
 								format_type_be(col->typeName->typeOid)),
 						 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted table column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				col->typeName = makeTypeNameFromOid(DatumGetObjectId(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attusertypid)),
+													DatumGetInt32(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attusertypmod)));
+				col->encryption = makeColumnEncryption(tp);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, col);
 		}
 	}
@@ -520,6 +539,17 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo)
 							format_type_be(col->typeName->typeOid)),
 					 errhint("Use the COLLATE clause to set the collation explicitly.")));
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			/*
+			 * We don't have the required information available here, so
+			 * prevent it for now.
+			 */
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("encrypted columns not yet implemented for this command")));
+		}
+
 		attrList = lappend(attrList, col);
 	}
 
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 92d983ac748..da2e54d3548 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -13,6 +13,7 @@
  */
 #include "postgres.h"
 
+#include "access/printtup.h"
 #include "access/xact.h"
 #include "catalog/namespace.h"
 #include "commands/async.h"
@@ -25,7 +26,7 @@
 static void DiscardAll(bool isTopLevel);
 
 /*
- * DISCARD { ALL | SEQUENCES | TEMP | PLANS }
+ * DISCARD
  */
 void
 DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
@@ -36,6 +37,10 @@ DiscardCommand(DiscardStmt *stmt, bool isTopLevel)
 			DiscardAll(isTopLevel);
 			break;
 
+		case DISCARD_COLUMN_ENCRYPTION_KEYS:
+			DiscardColumnEncryptionKeys();
+			break;
+
 		case DISCARD_PLANS:
 			ResetPlanCache();
 			break;
@@ -75,4 +80,5 @@ DiscardAll(bool isTopLevel)
 	ResetPlanCache();
 	ResetTempTableNamespace();
 	ResetSequenceCaches();
+	DiscardColumnEncryptionKeys();
 }
diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c
index 85eec7e3947..0205c03b132 100644
--- a/src/backend/commands/dropcmds.c
+++ b/src/backend/commands/dropcmds.c
@@ -271,6 +271,20 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 				name = NameListToString(castNode(List, object));
 			}
 			break;
+		case OBJECT_CEK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column encryption key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
+		case OBJECT_CMK:
+			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
+			{
+				msg = gettext_noop("column master key \"%s\" does not exist, skipping");
+				name = NameListToString(castNode(List, object));
+			}
+			break;
 		case OBJECT_CONVERSION:
 			if (!schema_does_not_exist_skipping(castNode(List, object), &msg, &name))
 			{
@@ -499,6 +513,7 @@ does_not_exist_skipping(ObjectType objtype, Node *object)
 		case OBJECT_AMOP:
 		case OBJECT_AMPROC:
 		case OBJECT_ATTRIBUTE:
+		case OBJECT_CEKDATA:
 		case OBJECT_DEFAULT:
 		case OBJECT_DEFACL:
 		case OBJECT_DOMCONSTRAINT:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 0d3214df9ca..605299e564d 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -2163,6 +2163,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:
@@ -2246,6 +2249,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 7549be5dc3b..008ed999daf 100644
--- a/src/backend/commands/meson.build
+++ b/src/backend/commands/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'analyze.c',
   'async.c',
   'cluster.c',
+  'colenccmds.c',
   'collationcmds.c',
   'comment.c',
   'constraint.c',
diff --git a/src/backend/commands/seclabel.c b/src/backend/commands/seclabel.c
index 5607273bf9f..feecb704e50 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 8a98a0af482..ede9d335be0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -36,6 +36,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"
@@ -64,6 +65,7 @@
 #include "commands/typecmds.h"
 #include "commands/user.h"
 #include "commands/vacuum.h"
+#include "common/colenc.h"
 #include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "foreign/foreign.h"
@@ -669,6 +671,8 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void GetColumnEncryption(ParseState *pstate, const List *coldefencryption,
+								FormData_pg_attribute *attr, FormExtraData_pg_attribute *attr_extra);
 
 static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab,
 								 Relation rel, PartitionCmd *cmd,
@@ -705,6 +709,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	Oid			tablespaceId;
 	Relation	rel;
 	TupleDesc	descriptor;
+	FormExtraData_pg_attribute *desc_extra;
 	List	   *inheritOids;
 	List	   *old_constraints;
 	List	   *old_notnulls;
@@ -937,7 +942,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	 * not default values, NOT NULL or CHECK constraints; we handle those
 	 * below.
 	 */
-	descriptor = BuildDescForRelation(stmt->tableElts);
+	descriptor = BuildDescForRelation(stmt->tableElts, &desc_extra);
 
 	/*
 	 * Find columns with default values and prepare for insertion of the
@@ -1010,6 +1015,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 										  ownerId,
 										  accessMethodId,
 										  descriptor,
+										  desc_extra,
 										  list_concat(cookedDefaults,
 													  old_constraints),
 										  relkind,
@@ -1315,12 +1321,13 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
  * Note: tdtypeid will need to be filled in later on.
  */
 TupleDesc
-BuildDescForRelation(const List *columns)
+BuildDescForRelation(const List *columns, FormExtraData_pg_attribute **tupdesc_extra_p)
 {
 	int			natts;
 	AttrNumber	attnum;
 	ListCell   *l;
 	TupleDesc	desc;
+	FormExtraData_pg_attribute *desc_extra;
 	bool		has_not_null;
 	char	   *attname;
 	Oid			atttypid;
@@ -1333,6 +1340,7 @@ BuildDescForRelation(const List *columns)
 	 */
 	natts = list_length(columns);
 	desc = CreateTemplateTupleDesc(natts);
+	desc_extra = palloc_array(FormExtraData_pg_attribute, natts);
 	has_not_null = false;
 
 	attnum = 0;
@@ -1341,7 +1349,8 @@ BuildDescForRelation(const List *columns)
 	{
 		ColumnDef  *entry = lfirst(l);
 		AclResult	aclresult;
-		Form_pg_attribute att;
+		FormData_pg_attribute *att;
+		FormExtraData_pg_attribute *att_extra;
 
 		/*
 		 * for each entry in the list, get the name and type information from
@@ -1373,10 +1382,17 @@ BuildDescForRelation(const List *columns)
 		TupleDescInitEntry(desc, attnum, attname,
 						   atttypid, atttypmod, attdim);
 		att = TupleDescAttr(desc, attnum - 1);
+		att_extra = &desc_extra[attnum - 1];
 
 		/* Override TupleDescInitEntry's settings as requested */
 		TupleDescInitEntryCollation(desc, attnum, attcollation);
 
+		att_extra->attstattarget.isnull = true;
+		att_extra->attoptions.isnull = true;
+		att_extra->attcek.isnull = true;
+		att_extra->attusertypid.isnull = true;
+		att_extra->attusertypmod.isnull = true;
+
 		/* Fill in additional stuff not handled by TupleDescInitEntry */
 		att->attnotnull = entry->is_not_null;
 		has_not_null |= entry->is_not_null;
@@ -1389,6 +1405,15 @@ BuildDescForRelation(const List *columns)
 			att->attstorage = entry->storage;
 		else if (entry->storage_name)
 			att->attstorage = GetAttributeStorage(att->atttypid, entry->storage_name);
+
+		if (entry->encryption)
+		{
+			GetColumnEncryption(NULL, entry->encryption, att, att_extra);
+			Assert(!att_extra->attcek.isnull);
+			aclresult = object_aclcheck(ColumnEncKeyRelationId, DatumGetObjectId(att_extra->attcek.value), GetUserId(), ACL_USAGE);
+			if (aclresult != ACLCHECK_OK)
+				aclcheck_error(aclresult, OBJECT_CEK, get_cek_name(DatumGetObjectId(att_extra->attcek.value), false));
+		}
 	}
 
 	if (has_not_null)
@@ -1409,6 +1434,7 @@ BuildDescForRelation(const List *columns)
 		desc->constr = NULL;
 	}
 
+	*tupdesc_extra_p = desc_extra;
 	return desc;
 }
 
@@ -2742,6 +2768,18 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 			 */
 			newdef = makeColumnDef(attributeName, attribute->atttypid,
 								   attribute->atttypmod, attribute->attcollation);
+			if (type_is_encrypted(attribute->atttypid))
+			{
+				HeapTuple	tp;
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(attribute->attrelid), Int16GetDatum(attribute->attnum));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", attribute->attnum, attribute->attrelid);
+				newdef->typeName = makeTypeNameFromOid(DatumGetObjectId(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attusertypid)),
+													   DatumGetInt32(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attusertypmod)));
+				newdef->encryption = makeColumnEncryption(tp);
+				ReleaseSysCache(tp);
+			}
 			newdef->storage = attribute->attstorage;
 			newdef->generated = attribute->attgenerated;
 			if (CompressionMethodIsValid(attribute->attcompression))
@@ -3300,6 +3338,32 @@ MergeChildAttribute(List *inh_columns, int exist_attno, int newcol_attno, const
 					 errdetail("%s versus %s", inhdef->compression, newdef->compression)));
 	}
 
+	/*
+	 * Check encryption parameter.  All parents and children must have the
+	 * same encryption settings for a column.
+	 */
+	if ((inhdef->encryption && !newdef->encryption) || (!inhdef->encryption && newdef->encryption))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("column \"%s\" has an encryption specification conflict",
+						attributeName)));
+	else if (inhdef->encryption && newdef->encryption)
+	{
+		FormData_pg_attribute inha,
+					newa;
+		FormExtraData_pg_attribute inhax,
+					newax;
+
+		GetColumnEncryption(NULL, inhdef->encryption, &inha, &inhax);
+		GetColumnEncryption(NULL, newdef->encryption, &newa, &newax);
+
+		if (inha.atttypid != newa.atttypid || inha.atttypmod != newa.atttypmod || inhax.attcek.value != newax.attcek.value)
+			ereport(ERROR,
+					(errcode(ERRCODE_DATATYPE_MISMATCH),
+					 errmsg("column \"%s\" has an encryption specification conflict",
+							attributeName)));
+	}
+
 	/*
 	 * Merge of not-null constraints = OR 'em together
 	 */
@@ -3394,6 +3458,29 @@ MergeInheritedAttribute(List *inh_columns,
 					attributeName)));
 	prevdef = list_nth_node(ColumnDef, inh_columns, exist_attno - 1);
 
+	/*
+	 * Check encryption parameter.  All parents must have the same encryption
+	 * settings for a column.
+	 */
+	if ((prevdef->encryption && !newdef->encryption) || (!prevdef->encryption && newdef->encryption))
+		ereport(ERROR,
+				(errcode(ERRCODE_DATATYPE_MISMATCH),
+				 errmsg("column \"%s\" has an encryption specification conflict",
+						attributeName)));
+	else if (prevdef->encryption && newdef->encryption)
+	{
+		/*
+		 * Merging the encryption properties of two encrypted parent columns
+		 * is not yet implemented.  Right now, this would confuse the checks
+		 * of the type etc. below (we must check the physical and the real
+		 * types against each other, respectively), which might require a
+		 * larger restructuring.  For now, just give up here.
+		 */
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("multiple inheritance of encrypted columns is not implemented")));
+	}
+
 	/*
 	 * Must have the same type and typmod
 	 */
@@ -7113,6 +7200,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	AlterTableCmd *childcmd;
 	ObjectAddress address;
 	TupleDesc	tupdesc;
+	FormExtraData_pg_attribute *tupdesc_extra;
 
 	/* since this function recurses, it could be driven to stack overflow */
 	check_stack_depth();
@@ -7250,7 +7338,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	/*
 	 * Construct new attribute's pg_attribute entry.
 	 */
-	tupdesc = BuildDescForRelation(list_make1(colDef));
+	tupdesc = BuildDescForRelation(list_make1(colDef), &tupdesc_extra);
 
 	attribute = TupleDescAttr(tupdesc, 0);
 
@@ -7260,9 +7348,10 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	/* make sure datatype is legal for a column */
 	CheckAttributeType(NameStr(attribute->attname), attribute->atttypid, attribute->attcollation,
 					   list_make1_oid(rel->rd_rel->reltype),
-					   0);
+					   !tupdesc_extra[0].attcek.isnull ? CHKATYPE_ENCRYPTED : 0);
+
 
-	InsertPgAttributeTuples(attrdesc, tupdesc, myrelid, NULL, NULL);
+	InsertPgAttributeTuples(attrdesc, tupdesc, myrelid, tupdesc_extra, NULL);
 
 	table_close(attrdesc, RowExclusiveLock);
 
@@ -20887,6 +20976,144 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 	return cstorage;
 }
 
+/*
+ * resolve column encryption specification
+ */
+static void
+GetColumnEncryption(ParseState *pstate, const List *coldefencryption,
+					FormData_pg_attribute *attr, FormExtraData_pg_attribute *attr_extra)
+{
+	ListCell   *lc;
+	DefElem    *cekEl = NULL;
+	DefElem    *encdetEl = NULL;
+	DefElem    *algEl = NULL;
+	Oid			cekoid;
+	bool		encdet;
+	int			alg;
+
+	foreach(lc, coldefencryption)
+	{
+		DefElem    *defel = lfirst_node(DefElem, lc);
+		DefElem   **defelp;
+
+		if (strcmp(defel->defname, "column_encryption_key") == 0)
+			defelp = &cekEl;
+		else if (strcmp(defel->defname, "encryption_type") == 0)
+			defelp = &encdetEl;
+		else if (strcmp(defel->defname, "algorithm") == 0)
+			defelp = &algEl;
+		else
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized column encryption parameter: %s", defel->defname),
+					parser_errposition(pstate, defel->location));
+		if (*defelp != NULL)
+			errorConflictingDefElem(defel, pstate);
+		*defelp = defel;
+	}
+
+	if (cekEl)
+	{
+		List	   *val = defGetQualifiedName(cekEl);
+
+		cekoid = get_cek_oid(val, false);
+	}
+	else
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_COLUMN_DEFINITION),
+				errmsg("column encryption key must be specified"));
+
+	if (encdetEl)
+	{
+		char	   *val = strVal(linitial(castNode(TypeName, encdetEl->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),
+					parser_errposition(pstate, encdetEl->location));
+	}
+	else
+		encdet = false;
+
+	if (algEl)
+	{
+		char	   *val = strVal(algEl->arg);
+
+		alg = get_cekalg_num(val);
+
+		if (!alg)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					errmsg("unrecognized encryption algorithm: %s", val),
+					parser_errposition(pstate, algEl->location));
+	}
+	else
+		alg = PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+
+	attr_extra->attcek.value = ObjectIdGetDatum(cekoid);
+	attr_extra->attusertypid.value = ObjectIdGetDatum(attr->atttypid);
+	attr_extra->attusertypmod.value = Int32GetDatum(attr->atttypmod);
+	attr_extra->attcek.isnull = attr_extra->attusertypid.isnull = attr_extra->attusertypmod.isnull = false;
+
+	/* 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;
+
+	attr->atttypmod = alg;
+}
+
+/*
+ * Construct input to GetColumnEncryption(), for synthesizing a column
+ * definition.
+ */
+List *
+makeColumnEncryption(HeapTuple attrtup)
+{
+	List	   *result;
+	HeapTuple	cektup;
+	Form_pg_colenckey cekform;
+	char	   *nspname;
+	Form_pg_attribute attr;
+	Oid			attcek;
+
+	attr = (Form_pg_attribute) GETSTRUCT(attrtup);
+
+	attcek = DatumGetObjectId(SysCacheGetAttrNotNull(ATTNUM, attrtup, Anum_pg_attribute_attcek));
+
+	cektup = SearchSysCache1(CEKOID, ObjectIdGetDatum(attcek));
+	if (!HeapTupleIsValid(cektup))
+		elog(ERROR, "cache lookup failed for column encryption key %u", attcek);
+
+	cekform = (Form_pg_colenckey) GETSTRUCT(cektup);
+	nspname = get_namespace_name(cekform->ceknamespace);
+
+	result = list_make3(makeDefElem("column_encryption_key",
+									(Node *) list_make2(makeString(nspname), makeString(pstrdup(NameStr(cekform->cekname)))),
+									-1),
+						makeDefElem("encryption_type",
+									(Node *) makeTypeName(attr->atttypid == PG_ENCRYPTED_DETOID ?
+														  "deterministic" : "randomized"),
+									-1),
+						makeDefElem("algorithm",
+									(Node *) makeString(pstrdup(get_cekalg_name(attr->atttypmod))),
+									-1));
+
+	ReleaseSysCache(cektup);
+
+	return result;
+}
+
 /*
  * Struct with context of new partition for insert rows from splited partition
  */
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index 01151ca2b5a..19362e34168 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/commands/view.c b/src/backend/commands/view.c
index fdad8338324..86f006d7c27 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -29,6 +29,7 @@
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 
 static void checkViewColumns(TupleDesc newdesc, TupleDesc olddesc);
 
@@ -83,6 +84,24 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 			else
 				Assert(!OidIsValid(def->collOid));
 
+			if (type_is_encrypted(exprType((Node *) tle->expr)))
+			{
+				HeapTuple	tp;
+
+				if (!tle->resorigtbl || !tle->resorigcol)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("underlying table and column could not be determined for encrypted view column"));
+
+				tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(tle->resorigtbl), Int16GetDatum(tle->resorigcol));
+				if (!HeapTupleIsValid(tp))
+					elog(ERROR, "cache lookup failed for attribute %d of relation %u", tle->resorigcol, tle->resorigtbl);
+				def->typeName = makeTypeNameFromOid(DatumGetObjectId(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attusertypid)),
+													DatumGetInt32(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attusertypmod)));
+				def->encryption = makeColumnEncryption(tp);
+				ReleaseSysCache(tp);
+			}
+
 			attrList = lappend(attrList, def);
 		}
 	}
@@ -100,6 +119,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 	{
 		Relation	rel;
 		TupleDesc	descriptor;
+		FormExtraData_pg_attribute *desc_extra;
 		List	   *atcmds = NIL;
 		AlterTableCmd *atcmd;
 		ObjectAddress address;
@@ -129,7 +149,7 @@ DefineVirtualRelation(RangeVar *relation, List *tlist, bool replace,
 		 * verify that the old column list is an initial prefix of the new
 		 * column list.
 		 */
-		descriptor = BuildDescForRelation(attrList);
+		descriptor = BuildDescForRelation(attrList, &desc_extra);
 		checkViewColumns(descriptor, rel->rd_att);
 
 		/*
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index e1df1894b69..18b9bfc2deb 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -4512,6 +4512,8 @@ raw_expression_tree_walker_impl(Node *node,
 
 				if (WALK(coldef->typeName))
 					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/gram.y b/src/backend/parser/gram.y
index 0523f7e891e..2b5537d020f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -283,6 +283,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <node>	stmt toplevel_stmt schema_stmt routine_body_stmt
 		AlterEventTrigStmt AlterCollationStmt
+		AlterColumnEncryptionKeyStmt AlterColumnMasterKeyStmt
 		AlterDatabaseStmt AlterDatabaseSetStmt AlterDomainStmt AlterEnumStmt
 		AlterFdwStmt AlterForeignServerStmt AlterGroupStmt
 		AlterObjectDependsStmt AlterObjectSchemaStmt AlterOwnerStmt
@@ -424,6 +425,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
@@ -599,6 +601,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
@@ -727,7 +730,7 @@ 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 EMPTY_P ENABLE_P ENCODING ENCRYPTED END_P ENUM_P ERROR_P ESCAPE
+	EACH ELSE EMPTY_P ENABLE_P ENCODING ENCRYPTED ENCRYPTION END_P ENUM_P ERROR_P ESCAPE
 	EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN EXPRESSION
 	EXTENSION EXTERNAL EXTRACT
 
@@ -752,7 +755,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 MERGE_ACTION METHOD
+	MAPPING MASTER MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGE_ACTION METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
 
 	NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO
@@ -1005,6 +1008,8 @@ toplevel_stmt:
 stmt:
 			AlterEventTrigStmt
 			| AlterCollationStmt
+			| AlterColumnEncryptionKeyStmt
+			| AlterColumnMasterKeyStmt
 			| AlterDatabaseStmt
 			| AlterDatabaseSetStmt
 			| AlterDefaultPrivilegesStmt
@@ -2031,7 +2036,7 @@ CheckPointStmt:
 
 /*****************************************************************************
  *
- * DISCARD { ALL | TEMP | PLANS | SEQUENCES }
+ * DISCARD
  *
  *****************************************************************************/
 
@@ -2057,6 +2062,13 @@ DiscardStmt:
 					n->target = DISCARD_TEMP;
 					$$ = (Node *) n;
 				}
+			| DISCARD COLUMN ENCRYPTION KEYS
+				{
+					DiscardStmt *n = makeNode(DiscardStmt);
+
+					n->target = DISCARD_COLUMN_ENCRYPTION_KEYS;
+					$$ = (Node *) n;
+				}
 			| DISCARD PLANS
 				{
 					DiscardStmt *n = makeNode(DiscardStmt);
@@ -3821,14 +3833,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;
@@ -3837,8 +3850,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;
@@ -3895,6 +3908,11 @@ opt_column_compression:
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+opt_column_encryption:
+			ENCRYPTED WITH '(' def_list ')'			{ $$ = $4; }
+			| /*EMPTY*/								{ $$ = NULL; }
+		;
+
 column_storage:
 			STORAGE ColId							{ $$ = $2; }
 			| STORAGE DEFAULT						{ $$ = pstrdup("default"); }
@@ -4158,6 +4176,7 @@ TableLikeOption:
 				| COMPRESSION		{ $$ = CREATE_TABLE_LIKE_COMPRESSION; }
 				| CONSTRAINTS		{ $$ = CREATE_TABLE_LIKE_CONSTRAINTS; }
 				| DEFAULTS			{ $$ = CREATE_TABLE_LIKE_DEFAULTS; }
+				| ENCRYPTED			{ $$ = CREATE_TABLE_LIKE_ENCRYPTED; }
 				| IDENTITY_P		{ $$ = CREATE_TABLE_LIKE_IDENTITY; }
 				| GENERATED			{ $$ = CREATE_TABLE_LIKE_GENERATED; }
 				| INDEXES			{ $$ = CREATE_TABLE_LIKE_INDEXES; }
@@ -6435,6 +6454,33 @@ 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
+				{
+					DefineStmt *n = makeNode(DefineStmt);
+
+					n->kind = OBJECT_CMK;
+					n->defnames = $5;
+					n->definition = NIL;
+					$$ = (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; }
@@ -6454,6 +6500,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)); }
@@ -6992,6 +7042,8 @@ object_type_any_name:
 			| INDEX									{ $$ = OBJECT_INDEX; }
 			| FOREIGN TABLE							{ $$ = OBJECT_FOREIGN_TABLE; }
 			| COLLATION								{ $$ = OBJECT_COLLATION; }
+			| COLUMN ENCRYPTION KEY					{ $$ = OBJECT_CEK; }
+			| COLUMN MASTER KEY						{ $$ = OBJECT_CMK; }
 			| CONVERSION_P							{ $$ = OBJECT_CONVERSION; }
 			| STATISTICS							{ $$ = OBJECT_STATISTIC_EXT; }
 			| TEXT_P SEARCH PARSER					{ $$ = OBJECT_TSPARSER; }
@@ -7803,6 +7855,24 @@ privilege_target:
 					n->objs = $2;
 					$$ = n;
 				}
+			| COLUMN ENCRYPTION KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CEK;
+					n->objs = $4;
+					$$ = n;
+				}
+			| COLUMN MASTER KEY any_name_list
+				{
+					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
+
+					n->targtype = ACL_TARGET_OBJECT;
+					n->objtype = OBJECT_CMK;
+					n->objs = $4;
+					$$ = n;
+				}
 			| DATABASE name_list
 				{
 					PrivTarget *n = (PrivTarget *) palloc(sizeof(PrivTarget));
@@ -9332,6 +9402,26 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name RENAME TO name
+				{
+					RenameStmt *n = makeNode(RenameStmt);
+
+					n->renameType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newname = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name RENAME TO name
 				{
 					RenameStmt *n = makeNode(RenameStmt);
@@ -10009,6 +10099,26 @@ AlterObjectSchemaStmt:
 					n->missing_ok = false;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name SET SCHEMA name
+				{
+					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newschema = $8;
+					n->missing_ok = false;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name SET SCHEMA name
 				{
 					AlterObjectSchemaStmt *n = makeNode(AlterObjectSchemaStmt);
@@ -10342,6 +10452,24 @@ AlterOwnerStmt: ALTER AGGREGATE aggregate_with_argtypes OWNER TO RoleSpec
 					n->newowner = $6;
 					$$ = (Node *) n;
 				}
+			| ALTER COLUMN ENCRYPTION KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CEK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
+			| ALTER COLUMN MASTER KEY any_name OWNER TO RoleSpec
+				{
+					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
+
+					n->objectType = OBJECT_CMK;
+					n->object = (Node *) $5;
+					n->newowner = $8;
+					$$ = (Node *) n;
+				}
 			| ALTER CONVERSION_P any_name OWNER TO RoleSpec
 				{
 					AlterOwnerStmt *n = makeNode(AlterOwnerStmt);
@@ -11512,6 +11640,52 @@ AlterCollationStmt: ALTER COLLATION any_name REFRESH VERSION_P
 		;
 
 
+/*****************************************************************************
+ *
+ *		ALTER COLUMN ENCRYPTION KEY
+ *
+ *****************************************************************************/
+
+AlterColumnEncryptionKeyStmt:
+			ALTER COLUMN ENCRYPTION KEY any_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 any_name DROP VALUE_P definition
+				{
+					AlterColumnEncryptionKeyStmt *n = makeNode(AlterColumnEncryptionKeyStmt);
+
+					n->cekname = $5;
+					n->isDrop = true;
+					n->definition = $8;
+					$$ = (Node *) n;
+				}
+		;
+
+
+/*****************************************************************************
+ *
+ *		ALTER COLUMN MASTER KEY
+ *
+ *****************************************************************************/
+
+AlterColumnMasterKeyStmt:
+			ALTER COLUMN MASTER KEY any_name definition
+				{
+					AlterColumnMasterKeyStmt *n = makeNode(AlterColumnMasterKeyStmt);
+
+					n->cmkname = $5;
+					n->definition = $6;
+					$$ = (Node *) n;
+				}
+		;
+
+
 /*****************************************************************************
  *
  *		ALTER SYSTEM
@@ -17639,6 +17813,7 @@ unreserved_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| ENUM_P
 			| ERROR_P
 			| ESCAPE
@@ -17707,6 +17882,7 @@ unreserved_keyword:
 			| LOCKED
 			| LOGGED
 			| MAPPING
+			| MASTER
 			| MATCH
 			| MATCHED
 			| MATERIALIZED
@@ -18218,6 +18394,7 @@ bare_label_keyword:
 			| ENABLE_P
 			| ENCODING
 			| ENCRYPTED
+			| ENCRYPTION
 			| END_P
 			| ENUM_P
 			| ERROR_P
@@ -18322,6 +18499,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 dbf1a7dff08..a0b6a905bd3 100644
--- a/src/backend/parser/parse_param.c
+++ b/src/backend/parser/parse_param.c
@@ -29,6 +29,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/nodeFuncs.h"
 #include "parser/parse_param.h"
+#include "parser/parsetree.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 
@@ -357,3 +358,159 @@ query_contains_extern_params_walker(Node *node, void *context)
 	return expression_tree_walker(node, query_contains_extern_params_walker,
 								  context);
 }
+
+/*
+ * Walk a query tree and find out what tables and columns a parameter is
+ * associated with.
+ *
+ * We need to find 1) parameters written directly into a table column, and 2)
+ * binary predicates relating a parameter to a table column.
+ *
+ * We just need to find Var and Param nodes in appropriate places.  We don't
+ * need to do harder things like looking through casts, since this is used for
+ * column encryption, and encrypted columns can't be usefully cast to
+ * anything.
+ */
+
+struct find_param_origs_context
+{
+	const Query *query;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
+};
+
+static bool
+find_param_origs_walker(Node *node, struct find_param_origs_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, OpExpr) || IsA(node, DistinctExpr) || IsA(node, NullIfExpr))
+	{
+		OpExpr	   *opexpr = (OpExpr *) node;
+
+		if (list_length(opexpr->args) == 2)
+		{
+			Node	   *lexpr = linitial(opexpr->args);
+			Node	   *rexpr = lsecond(opexpr->args);
+			Var		   *v = NULL;
+			Param	   *p = NULL;
+
+			if (IsA(lexpr, Var) && IsA(rexpr, Param))
+			{
+				v = castNode(Var, lexpr);
+				p = castNode(Param, rexpr);
+			}
+			else if (IsA(rexpr, Var) && IsA(lexpr, Param))
+			{
+				v = castNode(Var, rexpr);
+				p = castNode(Param, lexpr);
+			}
+
+			if (v && p)
+			{
+				RangeTblEntry *rte;
+
+				rte = rt_fetch(v->varno, context->query->rtable);
+				if (rte->rtekind == RTE_RELATION)
+				{
+					context->param_orig_tbls[p->paramid - 1] = rte->relid;
+					context->param_orig_cols[p->paramid - 1] = v->varattno;
+				}
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * TargetEntry in a query with a result relation
+	 */
+	if (IsA(node, TargetEntry) && context->query->resultRelation > 0)
+	{
+		TargetEntry *te = (TargetEntry *) node;
+		RangeTblEntry *resrte;
+
+		resrte = rt_fetch(context->query->resultRelation, context->query->rtable);
+		if (resrte->rtekind == RTE_RELATION)
+		{
+			Expr	   *expr = te->expr;
+
+			/*
+			 * If it's a RelabelType, look inside.  (For encrypted columns,
+			 * this would typically be a typmod adjustment.)
+			 */
+			if (IsA(expr, RelabelType))
+				expr = castNode(RelabelType, expr)->arg;
+
+			/*
+			 * Param directly in a target list
+			 */
+			if (IsA(expr, Param))
+			{
+				Param	   *p = (Param *) expr;
+
+				context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+				context->param_orig_cols[p->paramid - 1] = te->resno;
+			}
+
+			/*
+			 * If it's a Var, check whether it corresponds to a VALUES list
+			 * with top-level parameters.  This covers multi-row INSERTS.
+			 */
+			else if (IsA(expr, Var))
+			{
+				Var		   *v = (Var *) expr;
+				RangeTblEntry *srcrte;
+
+				srcrte = rt_fetch(v->varno, context->query->rtable);
+				if (srcrte->rtekind == RTE_VALUES)
+				{
+					ListCell   *lc;
+
+					foreach(lc, srcrte->values_lists)
+					{
+						List	   *values_list = lfirst_node(List, lc);
+						Expr	   *value = list_nth(values_list, v->varattno - 1);
+
+						if (IsA(value, RelabelType))
+							value = castNode(RelabelType, value)->arg;
+
+						if (IsA(value, Param))
+						{
+							Param	   *p = (Param *) value;
+
+							context->param_orig_tbls[p->paramid - 1] = resrte->relid;
+							context->param_orig_cols[p->paramid - 1] = te->resno;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		return query_tree_walker((Query *) node, find_param_origs_walker, context, 0);
+	}
+
+	return expression_tree_walker(node, find_param_origs_walker, context);
+}
+
+void
+find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols)
+{
+	struct find_param_origs_context context;
+	ListCell   *lc;
+
+	context.param_orig_tbls = *param_orig_tbls;
+	context.param_orig_cols = *param_orig_cols;
+
+	foreach(lc, query_list)
+	{
+		Query	   *query = lfirst_node(Query, lc);
+
+		context.query = query;
+		query_tree_walker(query, find_param_origs_walker, &context, 0);
+	}
+}
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 7ca793a369f..0da07f0e68e 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -1945,7 +1945,7 @@ addRangeTableEntryForFunction(ParseState *pstate,
 			 * coldeflist doesn't represent anything that will be visible to
 			 * other sessions.
 			 */
-			CheckAttributeNamesTypes(tupdesc, RELKIND_COMPOSITE_TYPE,
+			CheckAttributeNamesTypes(tupdesc, NULL, RELKIND_COMPOSITE_TYPE,
 									 CHKATYPE_ANYRECORD);
 		}
 		else
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index ceba0699050..2680d6d06b6 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1092,6 +1092,20 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid,
 							attribute->atttypmod, attribute->attcollation);
 
+		if (type_is_encrypted(attribute->atttypid))
+		{
+			HeapTuple	tp;
+
+			tp = SearchSysCache2(ATTNUM, ObjectIdGetDatum(attribute->attrelid), Int16GetDatum(attribute->attnum));
+			if (!HeapTupleIsValid(tp))
+				elog(ERROR, "cache lookup failed for attribute %d of relation %u", attribute->attnum, attribute->attrelid);
+			def->typeName = makeTypeNameFromOid(DatumGetObjectId(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attusertypid)),
+												DatumGetInt32(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attusertypmod)));
+			if (table_like_clause->options & CREATE_TABLE_LIKE_ENCRYPTED)
+				def->encryption = makeColumnEncryption(tp);
+			ReleaseSysCache(tp);
+		}
+
 		/*
 		 * For constraints, ONLY the not-null constraint is inherited by the
 		 * new column definition per SQL99; however we cannot do that
diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index ee73d01e16c..33031795bd2 100644
--- a/src/backend/tcop/backend_startup.c
+++ b/src/backend/tcop/backend_startup.c
@@ -743,12 +743,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 76f48b13d20..4bef29e0c44 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -49,6 +49,7 @@
 #include "nodes/print.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
+#include "parser/parse_param.h"
 #include "parser/parser.h"
 #include "pg_getopt.h"
 #include "pg_trace.h"
@@ -77,6 +78,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"
 
@@ -1845,6 +1847,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 $%d corresponds to an encrypted column, but the parameter value was not encrypted", paramno + 1)));
+			}
+
 			if (pformat == 0)	/* text mode */
 			{
 				Oid			typinput;
@@ -2597,6 +2609,8 @@ static void
 exec_describe_statement_message(const char *stmt_name)
 {
 	CachedPlanSource *psrc;
+	Oid		   *param_orig_tbls;
+	AttrNumber *param_orig_cols;
 
 	/*
 	 * Start up a transaction command. (Note that this will normally change
@@ -2655,11 +2669,61 @@ exec_describe_statement_message(const char *stmt_name)
 														 * message type */
 	pq_sendint16(&row_description_buf, psrc->num_params);
 
+	/*
+	 * If column encryption is enabled, find the associated tables and columns
+	 * for any parameters, so that we can determine encryption information for
+	 * them.
+	 */
+	if (MyProcPort->column_encryption_enabled && psrc->num_params)
+	{
+		param_orig_tbls = palloc0_array(Oid, psrc->num_params);
+		param_orig_cols = palloc0_array(AttrNumber, psrc->num_params);
+
+		RevalidateCachedQuery(psrc, NULL);
+		find_param_origs(psrc->query_list, &param_orig_tbls, &param_orig_cols);
+	}
+
 	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 = param_orig_tbls[i];
+			AttrNumber	porigcol = param_orig_cols[i];
+			HeapTuple	tp;
+			Form_pg_attribute orig_att;
+
+			if (porigtbl == InvalidOid || porigcol == InvalidAttrNumber)
+				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 + 1)));
+
+			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 = DatumGetObjectId(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attusertypid));
+			pcekid = DatumGetObjectId(SysCacheGetAttrNotNull(ATTNUM, tp, Anum_pg_attribute_attcek));
+			pcekalg = orig_att->atttypmod;
+			ReleaseSysCache(tp);
+
+			if (psrc->param_types[i] == PG_ENCRYPTED_DETOID)
+				pflags |= 0x0001;
+
+			MaybeSendColumnEncryptionKeyMessage(pcekid);
+		}
 
 		pq_sendint32(&row_description_buf, (int) ptype);
+		if (MyProcPort->column_encryption_enabled)
+		{
+			pq_sendint32(&row_description_buf, (int) pcekid);
+			pq_sendint32(&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 c6bb3e45da4..dc3551fa0b9 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -27,6 +27,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"
@@ -131,6 +132,8 @@ ClassifyUtilityCommandAsReadOnly(Node *parsetree)
 	switch (nodeTag(parsetree))
 	{
 		case T_AlterCollationStmt:
+		case T_AlterColumnEncryptionKeyStmt:
+		case T_AlterColumnMasterKeyStmt:
 		case T_AlterDatabaseRefreshCollStmt:
 		case T_AlterDatabaseSetStmt:
 		case T_AlterDatabaseStmt:
@@ -1456,6 +1459,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,
@@ -1932,6 +1943,14 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = AlterCollation((AlterCollationStmt *) parsetree);
 				break;
 
+			case T_AlterColumnEncryptionKeyStmt:
+				address = AlterColumnEncryptionKey(pstate, (AlterColumnEncryptionKeyStmt *) parsetree);
+				break;
+
+			case T_AlterColumnMasterKeyStmt:
+				address = AlterColumnMasterKey(pstate, (AlterColumnMasterKeyStmt *) parsetree);
+				break;
+
 			default:
 				elog(ERROR, "unrecognized node type: %d",
 					 (int) nodeTag(parsetree));
@@ -2253,6 +2272,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;
@@ -2668,6 +2693,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;
 			}
@@ -2788,6 +2819,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;
@@ -2945,6 +2982,9 @@ CreateCommandTag(Node *parsetree)
 				case DISCARD_ALL:
 					tag = CMDTAG_DISCARD_ALL;
 					break;
+				case DISCARD_COLUMN_ENCRYPTION_KEYS:
+					tag = CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS;
+					break;
 				case DISCARD_PLANS:
 					tag = CMDTAG_DISCARD_PLANS;
 					break;
@@ -3091,6 +3131,14 @@ CreateCommandTag(Node *parsetree)
 			tag = CMDTAG_ALTER_COLLATION;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_ENCRYPTION_KEY;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			tag = CMDTAG_ALTER_COLUMN_MASTER_KEY;
+			break;
+
 		case T_PrepareStmt:
 			tag = CMDTAG_PREPARE;
 			break;
@@ -3716,6 +3764,14 @@ GetCommandLogLevel(Node *parsetree)
 			lev = LOGSTMT_DDL;
 			break;
 
+		case T_AlterColumnEncryptionKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
+		case T_AlterColumnMasterKeyStmt:
+			lev = LOGSTMT_DDL;
+			break;
+
 			/* already-planned queries */
 		case T_PlannedStmt:
 			{
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index dc10b4a4839..07bcdea79aa 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -22,6 +22,8 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_class.h"
+#include "catalog/pg_colenckey.h"
+#include "catalog/pg_colmasterkey.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_foreign_data_wrapper.h"
 #include "catalog/pg_foreign_server.h"
@@ -107,6 +109,10 @@ static AclMode convert_table_priv_string(text *priv_type_text);
 static AclMode convert_sequence_priv_string(text *priv_type_text);
 static AttrNumber convert_column_name(Oid tableoid, text *column);
 static AclMode convert_column_priv_string(text *priv_type_text);
+static Oid	convert_column_encryption_key_name(text *cekname);
+static AclMode convert_column_encryption_key_priv_string(text *priv_type_text);
+static Oid	convert_column_master_key_name(text *cmkname);
+static AclMode convert_column_master_key_priv_string(text *priv_type_text);
 static Oid	convert_database_name(text *databasename);
 static AclMode convert_database_priv_string(text *priv_type_text);
 static Oid	convert_foreign_data_wrapper_name(text *fdwname);
@@ -806,6 +812,14 @@ acldefault(ObjectType objtype, Oid ownerId)
 			world_default = ACL_NO_RIGHTS;
 			owner_default = ACL_ALL_RIGHTS_SEQUENCE;
 			break;
+		case OBJECT_CEK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CEK;
+			break;
+		case OBJECT_CMK:
+			world_default = ACL_NO_RIGHTS;
+			owner_default = ACL_ALL_RIGHTS_CMK;
+			break;
 		case OBJECT_DATABASE:
 			/* for backwards compatibility, grant some rights by default */
 			world_default = ACL_CREATE_TEMP | ACL_CONNECT;
@@ -917,6 +931,12 @@ acldefault_sql(PG_FUNCTION_ARGS)
 		case 's':
 			objtype = OBJECT_SEQUENCE;
 			break;
+		case 'Y':
+			objtype = OBJECT_CEK;
+			break;
+		case 'y':
+			objtype = OBJECT_CMK;
+			break;
 		case 'd':
 			objtype = OBJECT_DATABASE;
 			break;
@@ -2948,6 +2968,384 @@ convert_column_priv_string(text *priv_type_text)
 }
 
 
+/*
+ * has_column_encryption_key_privilege variants
+ *		These are all named "has_column_encryption_key_privilege" at the SQL level.
+ *		They take various combinations of column encryption key name,
+ *		cek OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_encryption_key_privilege_name_name
+ *		Check user privileges on a column encryption key given
+ *		name username, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name
+ *		Check user privileges on a column encryption key given
+ *		text cekname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cekname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_name_id
+ *		Check user privileges on a column encryption key given
+ *		name usename, column encryption key oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id
+ *		Check user privileges on a column encryption key given
+ *		column encryption key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_encryption_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cekid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_name
+ *		Check user privileges on a column encryption key given
+ *		roleid, text cekname, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cekname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cekid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cekid = convert_column_encryption_key_name(cekname);
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_encryption_key_privilege_id_id
+ *		Check user privileges on a column encryption key given
+ *		roleid, cek oid, and text priv name.
+ */
+Datum
+has_column_encryption_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cekid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_encryption_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CEKOID, ObjectIdGetDatum(cekid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnEncKeyRelationId, cekid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_encryption_key_privilege family.
+ */
+
+/*
+ * Given a CEK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_encryption_key_name(text *cekname)
+{
+	return get_cek_oid(textToQualifiedNameList(cekname), false);
+}
+
+/*
+ * convert_column_encryption_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_encryption_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_encryption_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_encryption_key_priv_map);
+}
+
+
+/*
+ * has_column_master_key_privilege variants
+ *		These are all named "has_column_master_key_privilege" at the SQL level.
+ *		They take various combinations of column master key name,
+ *		cmk OID, user name, user OID, or implicit user = current_user.
+ *
+ *		The result is a boolean value: true if user has the indicated
+ *		privilege, false if not.
+ */
+
+/*
+ * has_column_master_key_privilege_name_name
+ *		Check user privileges on a column master key given
+ *		name username, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_name(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name
+ *		Check user privileges on a column master key given
+ *		text cmkname and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_name(PG_FUNCTION_ARGS)
+{
+	text	   *cmkname = PG_GETARG_TEXT_PP(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_name_id
+ *		Check user privileges on a column master key given
+ *		name usename, column master key oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_name_id(PG_FUNCTION_ARGS)
+{
+	Name		username = PG_GETARG_NAME(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = get_role_oid_or_public(NameStr(*username));
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id
+ *		Check user privileges on a column master key given
+ *		column master key oid, and text priv name.
+ *		current_user is assumed
+ */
+Datum
+has_column_master_key_privilege_id(PG_FUNCTION_ARGS)
+{
+	Oid			cmkid = PG_GETARG_OID(0);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(1);
+	Oid			roleid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	roleid = GetUserId();
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_name
+ *		Check user privileges on a column master key given
+ *		roleid, text cmkname, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_name(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	text	   *cmkname = PG_GETARG_TEXT_PP(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	Oid			cmkid;
+	AclMode		mode;
+	AclResult	aclresult;
+
+	cmkid = convert_column_master_key_name(cmkname);
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ * has_column_master_key_privilege_id_id
+ *		Check user privileges on a column master key given
+ *		roleid, cmk oid, and text priv name.
+ */
+Datum
+has_column_master_key_privilege_id_id(PG_FUNCTION_ARGS)
+{
+	Oid			roleid = PG_GETARG_OID(0);
+	Oid			cmkid = PG_GETARG_OID(1);
+	text	   *priv_type_text = PG_GETARG_TEXT_PP(2);
+	AclMode		mode;
+	AclResult	aclresult;
+
+	mode = convert_column_master_key_priv_string(priv_type_text);
+
+	if (!SearchSysCacheExists1(CMKOID, ObjectIdGetDatum(cmkid)))
+		PG_RETURN_NULL();
+
+	aclresult = object_aclcheck(ColumnMasterKeyRelationId, cmkid, roleid, mode);
+
+	PG_RETURN_BOOL(aclresult == ACLCHECK_OK);
+}
+
+/*
+ *		Support routines for has_column_master_key_privilege family.
+ */
+
+/*
+ * Given a CMK name expressed as a string, look it up and return Oid
+ */
+static Oid
+convert_column_master_key_name(text *cmkname)
+{
+	return get_cmk_oid(textToQualifiedNameList(cmkname), false);
+}
+
+/*
+ * convert_column_master_key_priv_string
+ *		Convert text string to AclMode value.
+ */
+static AclMode
+convert_column_master_key_priv_string(text *priv_type_text)
+{
+	static const priv_map column_master_key_priv_map[] = {
+		{"USAGE", ACL_USAGE},
+		{"USAGE WITH GRANT OPTION", ACL_GRANT_OPTION_FOR(ACL_USAGE)},
+		{NULL, 0}
+	};
+
+	return convert_any_priv_string(priv_type_text, column_master_key_priv_map);
+}
+
+
 /*
  * has_database_privilege variants
  *		These are all named "has_database_privilege" at the SQL level.
diff --git a/src/backend/utils/adt/varlena.c b/src/backend/utils/adt/varlena.c
index d1b09dedfd4..8be950e6114 100644
--- a/src/backend/utils/adt/varlena.c
+++ b/src/backend/utils/adt/varlena.c
@@ -681,6 +681,113 @@ unknownsend(PG_FUNCTION_ARGS)
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
 }
 
+/*
+ * pg_encrypted_in -
+ *
+ * Input function for pg_encrypted_* types.
+ *
+ * The format and additional checks ensure that one cannot easily insert a
+ * value directly into an encrypted column by accident.  (That's why we don't
+ * just use the bytea format, for example.)  But we still have to support
+ * direct inserts into encrypted columns, for example for restoring backups
+ * made by pg_dump.
+ */
+Datum
+pg_encrypted_in(PG_FUNCTION_ARGS)
+{
+	char	   *inputText = PG_GETARG_CSTRING(0);
+	Node	   *escontext = fcinfo->context;
+	char	   *ip;
+	size_t		hexlen;
+	int			bc;
+	bytea	   *result;
+
+	if (strncmp(inputText, "encrypted$", 10) != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	ip = inputText + 10;
+	hexlen = strlen(ip);
+
+	/* sanity check to catch obvious mistakes */
+	if (hexlen / 2 < 32 || (hexlen / 2) % 16 != 0)
+		ereturn(escontext, (Datum) 0,
+				errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+				errmsg("invalid input value for encrypted column value: \"%s\"",
+					   inputText));
+
+	bc = hexlen / 2 + VARHDRSZ; /* maximum possible length */
+	result = palloc(bc);
+	bc = hex_decode(ip, hexlen, VARDATA(result));
+	SET_VARSIZE(result, bc + VARHDRSZ); /* actual length */
+
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_out -
+ *
+ * Output function for pg_encrypted_* types.
+ *
+ * This output is seen when reading an encrypted column without column
+ * encryption mode enabled.  Therefore, the output format is chosen so that it
+ * is easily recognizable.
+ */
+Datum
+pg_encrypted_out(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_PP(0);
+	char	   *result;
+	char	   *rp;
+
+	rp = result = palloc(VARSIZE_ANY_EXHDR(vlena) * 2 + 10 + 1);
+	memcpy(rp, "encrypted$", 10);
+	rp += 10;
+	rp += hex_encode(VARDATA_ANY(vlena), VARSIZE_ANY_EXHDR(vlena), rp);
+	*rp = '\0';
+	PG_RETURN_CSTRING(result);
+}
+
+/*
+ * pg_encrypted_recv -
+ *
+ * Receive function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_recv(PG_FUNCTION_ARGS)
+{
+	StringInfo	buf = (StringInfo) PG_GETARG_POINTER(0);
+	bytea	   *result;
+	int			nbytes;
+
+	nbytes = buf->len - buf->cursor;
+	/* sanity check to catch obvious mistakes */
+	if (nbytes < 32)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+				errmsg("invalid binary input value for encrypted column value"));
+	result = (bytea *) palloc(nbytes + VARHDRSZ);
+	SET_VARSIZE(result, nbytes + VARHDRSZ);
+	pq_copymsgbytes(buf, VARDATA(result), nbytes);
+	PG_RETURN_BYTEA_P(result);
+}
+
+/*
+ * pg_encrypted_send -
+ *
+ * Send function for pg_encrypted_* types.
+ */
+Datum
+pg_encrypted_send(PG_FUNCTION_ARGS)
+{
+	bytea	   *vlena = PG_GETARG_BYTEA_P_COPY(0);
+
+	/* just return input */
+	PG_RETURN_BYTEA_P(vlena);
+}
+
 
 /* ========== PUBLIC ROUTINES ========== */
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 26368ffcc97..9247475f58b 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -24,7 +24,10 @@
 #include "catalog/pg_amproc.h"
 #include "catalog/pg_cast.h"
 #include "catalog/pg_class.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_index.h"
 #include "catalog/pg_language.h"
@@ -2678,6 +2681,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
  *
@@ -3692,3 +3714,64 @@ get_subscription_name(Oid subid, bool missing_ok)
 
 	return subname;
 }
+
+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;
+}
+
+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 5af1a168ec2..eb0c6645f07 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -100,8 +100,6 @@ static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
-static List *RevalidateCachedQuery(CachedPlanSource *plansource,
-								   QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
 static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
 								   ParamListInfo boundParams, QueryEnvironment *queryEnv);
@@ -579,7 +577,7 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
  * had to do re-analysis, and NIL otherwise.  (This is returned just to save
  * a tree copying step in a subsequent BuildCachedPlan call.)
  */
-static List *
+List *
 RevalidateCachedQuery(CachedPlanSource *plansource,
 					  QueryEnvironment *queryEnv)
 {
diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c
index 62777b14c9b..e8b9296830d 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/fmgrprotos.h"
 #include "utils/memutils.h"
 #include "varatt.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 c7dd0b11fd2..7a9082ee6ca 100644
--- a/src/bin/pg_dump/common.c
+++ b/src/bin/pg_dump/common.c
@@ -18,7 +18,9 @@
 #include <ctype.h>
 
 #include "catalog/pg_class_d.h"
+#include "catalog/pg_colenckey_d.h"
 #include "catalog/pg_collation_d.h"
+#include "catalog/pg_colmasterkey_d.h"
 #include "catalog/pg_extension_d.h"
 #include "catalog/pg_namespace_d.h"
 #include "catalog/pg_operator_d.h"
@@ -203,6 +205,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);
 
@@ -936,6 +944,42 @@ findOprByOid(Oid oid)
 	return (OprInfo *) dobj;
 }
 
+/*
+ * findCekByOid
+ *	  finds the DumpableObject for the CEK with the given oid
+ *	  returns NULL if not found
+ */
+CekInfo *
+findCekByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnEncKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CEK);
+	return (CekInfo *) dobj;
+}
+
+/*
+ * findCmkByOid
+ *	  finds the DumpableObject for the CMK with the given oid
+ *	  returns NULL if not found
+ */
+CmkInfo *
+findCmkByOid(Oid oid)
+{
+	CatalogId	catId;
+	DumpableObject *dobj;
+
+	catId.tableoid = ColumnMasterKeyRelationId;
+	catId.oid = oid;
+	dobj = findObjectByCatalogId(catId);
+	Assert(dobj == NULL || dobj->objType == DO_CMK);
+	return (CmkInfo *) dobj;
+}
+
 /*
  * findCollationByOid
  *	  finds the DumpableObject for the collation with the given oid
diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c
index 5649859aa1e..1ca0d33fa3d 100644
--- a/src/bin/pg_dump/dumputils.c
+++ b/src/bin/pg_dump/dumputils.c
@@ -484,6 +484,10 @@ do { \
 		CONVERT_PRIV('C', "CREATE");
 		CONVERT_PRIV('U', "USAGE");
 	}
+	else if (strcmp(type, "COLUMN ENCRYPTION KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
+	else if (strcmp(type, "COLUMN MASTER KEY") == 0)
+		CONVERT_PRIV('U', "USAGE");
 	else if (strcmp(type, "DATABASE") == 0)
 	{
 		CONVERT_PRIV('C', "CREATE");
diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index fbf5f1c515e..f8d8d5a97dc 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -86,6 +86,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 465e9ce777f..cd2c554336a 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -3581,6 +3581,8 @@ _getObjectDescription(PQExpBuffer buf, const 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, "FOREIGN TABLE") == 0 ||
diff --git a/src/bin/pg_dump/pg_backup_db.c b/src/bin/pg_dump/pg_backup_db.c
index a02841c4050..47cfa5bab2c 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 c52e961b309..1ae914182f3 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -54,6 +54,7 @@
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_trigger_d.h"
 #include "catalog/pg_type_d.h"
+#include "common/colenc.h"
 #include "common/connect.h"
 #include "common/relpath.h"
 #include "compress_io.h"
@@ -245,6 +246,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);
@@ -414,6 +417,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},
@@ -742,6 +746,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");
 
@@ -1128,6 +1135,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"
@@ -6071,6 +6079,164 @@ 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_ceknamespace;
+	int			i_cekowner;
+	int			i_cekacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 170000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cek.tableoid, cek.oid, cek.cekname, cek.ceknamespace, cek.cekowner, cek.cekacl, acldefault('Y', cek.cekowner) AS acldefault\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_ceknamespace = PQfnumber(res, "ceknamespace");
+	i_cekowner = PQfnumber(res, "cekowner");
+	i_cekacl = PQfnumber(res, "cekacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_ceknamespace)));
+		cekinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cekacl));
+		cekinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cekinfo[i].dacl.privtype = 0;
+		cekinfo[i].dacl.initprivs = NULL;
+		cekinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cekowner));
+
+		resetPQExpBuffer(query);
+		appendPQExpBuffer(query,
+						  "SELECT ckdcmkid, 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].cekcmks = pg_malloc(sizeof(CmkInfo *) * ntups2);
+		cekinfo[i].cekcmkalgs = pg_malloc(sizeof(int) * ntups2);
+		cekinfo[i].cekencvals = pg_malloc(sizeof(char *) * ntups2);
+		for (int j = 0; j < ntups2; j++)
+		{
+			Oid			ckdcmkid;
+
+			ckdcmkid = atooid(PQgetvalue(res2, j, PQfnumber(res2, "ckdcmkid")));
+			cekinfo[i].cekcmks[j] = findCmkByOid(ckdcmkid);
+			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);
+		if (!PQgetisnull(res, i, i_cekacl))
+			cekinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	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_cmknamespace;
+	int			i_cmkowner;
+	int			i_cmkrealm;
+	int			i_cmkacl;
+	int			i_acldefault;
+
+	if (fout->remoteVersion < 170000)
+		return;
+
+	query = createPQExpBuffer();
+
+	appendPQExpBuffer(query,
+					  "SELECT cmk.tableoid, cmk.oid, cmk.cmkname, cmk.cmknamespace, cmk.cmkowner, cmk.cmkrealm, cmk.cmkacl, acldefault('y', cmk.cmkowner) AS acldefault\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_cmknamespace = PQfnumber(res, "cmknamespace");
+	i_cmkowner = PQfnumber(res, "cmkowner");
+	i_cmkrealm = PQfnumber(res, "cmkrealm");
+	i_cmkacl = PQfnumber(res, "cmkacl");
+	i_acldefault = PQfnumber(res, "acldefault");
+
+	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].dobj.namespace = findNamespace(atooid(PQgetvalue(res, i, i_cmknamespace)));
+		cmkinfo[i].dacl.acl = pg_strdup(PQgetvalue(res, i, i_cmkacl));
+		cmkinfo[i].dacl.acldefault = pg_strdup(PQgetvalue(res, i, i_acldefault));
+		cmkinfo[i].dacl.privtype = 0;
+		cmkinfo[i].dacl.initprivs = NULL;
+		cmkinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_cmkowner));
+		cmkinfo[i].cmkrealm = pg_strdup(PQgetvalue(res, i, i_cmkrealm));
+
+		selectDumpableObject(&(cmkinfo[i].dobj), fout);
+		if (!PQgetisnull(res, i, i_cmkacl))
+			cmkinfo[i].dobj.components |= DUMP_COMPONENT_ACL;
+	}
+	PQclear(res);
+
+	destroyPQExpBuffer(query);
+}
+
 /*
  * getConversions:
  *	  read all conversions in the system catalogs and return them in the
@@ -8701,6 +8867,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_typstorage;
 	int			i_attidentity;
 	int			i_attgenerated;
+	int			i_attcek;
+	int			i_attencalg;
+	int			i_attencdet;
 	int			i_attisdropped;
 	int			i_attlen;
 	int			i_attalign;
@@ -8763,29 +8932,31 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * collation is different from their type's default, we use a CASE here to
 	 * suppress uninteresting attcollations cheaply.
 	 */
-	appendPQExpBufferStr(q,
-						 "SELECT\n"
-						 "a.attrelid,\n"
-						 "a.attnum,\n"
-						 "a.attname,\n"
-						 "a.attstattarget,\n"
-						 "a.attstorage,\n"
-						 "t.typstorage,\n"
-						 "a.atthasdef,\n"
-						 "a.attisdropped,\n"
-						 "a.attlen,\n"
-						 "a.attalign,\n"
-						 "a.attislocal,\n"
-						 "pg_catalog.format_type(t.oid, a.atttypmod) AS atttypname,\n"
-						 "array_to_string(a.attoptions, ', ') AS attoptions,\n"
-						 "CASE WHEN a.attcollation <> t.typcollation "
-						 "THEN a.attcollation ELSE 0 END AS attcollation,\n"
-						 "pg_catalog.array_to_string(ARRAY("
-						 "SELECT pg_catalog.quote_ident(option_name) || "
-						 "' ' || pg_catalog.quote_literal(option_value) "
-						 "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
-						 "ORDER BY option_name"
-						 "), E',\n    ') AS attfdwoptions,\n");
+	appendPQExpBuffer(q, "SELECT\n"
+					  "a.attrelid,\n"
+					  "a.attnum,\n"
+					  "a.attname,\n"
+					  "a.attstattarget,\n"
+					  "a.attstorage,\n"
+					  "t.typstorage,\n"
+					  "a.atthasdef,\n"
+					  "a.attisdropped,\n"
+					  "a.attlen,\n"
+					  "a.attalign,\n"
+					  "a.attislocal,\n"
+					  "pg_catalog.format_type(%s) AS atttypname,\n"
+					  "array_to_string(a.attoptions, ', ') AS attoptions,\n"
+					  "CASE WHEN a.attcollation <> t.typcollation "
+					  "THEN a.attcollation ELSE 0 END AS attcollation,\n"
+					  "pg_catalog.array_to_string(ARRAY("
+					  "SELECT pg_catalog.quote_ident(option_name) || "
+					  "' ' || pg_catalog.quote_literal(option_value) "
+					  "FROM pg_catalog.pg_options_to_table(attfdwoptions) "
+					  "ORDER BY option_name"
+					  "), E',\n    ') AS attfdwoptions,\n",
+					  fout->remoteVersion >= 170000 ?
+					  "CASE WHEN a.attusertypid IS NOT NULL THEN a.attusertypid ELSE a.atttypid END, CASE WHEN a.attusertypid IS NOT NULL THEN a.attusertypmod ELSE a.atttypmod END" :
+					  "a.atttypid, a.atttypmod");
 
 	/*
 	 * Find out any NOT NULL markings for each column.  In 17 and up we have
@@ -8839,10 +9010,23 @@ 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 >= 170000)
+		appendPQExpBuffer(q,
+						  "a.attcek,\n"
+						  "CASE a.atttypid WHEN %u THEN true WHEN %u THEN false END AS attencdet,\n"
+						  "CASE WHEN a.atttypid IN (%u, %u) THEN a.atttypmod END AS attencalg\n",
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID,
+						  PG_ENCRYPTED_DETOID, PG_ENCRYPTED_RNDOID);
 	else
 		appendPQExpBufferStr(q,
-							 "'' AS attgenerated\n");
+							 "NULL AS attcek,\n"
+							 "NULL AS attencdet,\n"
+							 "NULL AS attencalg\n");
 
 	/* need left join to pg_type to not fail on dropped columns ... */
 	appendPQExpBuffer(q,
@@ -8885,6 +9069,9 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_typstorage = PQfnumber(res, "typstorage");
 	i_attidentity = PQfnumber(res, "attidentity");
 	i_attgenerated = PQfnumber(res, "attgenerated");
+	i_attcek = PQfnumber(res, "attcek");
+	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");
@@ -8951,6 +9138,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->attcek = (CekInfo **) pg_malloc(numatts * sizeof(CekInfo *));
+		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));
@@ -8987,6 +9177,22 @@ 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_attcek))
+			{
+				Oid			attcekid = atooid(PQgetvalue(res, r, i_attcek));
+
+				tbinfo->attcek[j] = findCekByOid(attcekid);
+			}
+			else
+				tbinfo->attcek[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));
@@ -10625,6 +10831,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;
@@ -14100,6 +14312,141 @@ dumpCollation(Archive *fout, const CollInfo *collinfo)
 	free(qcollname);
 }
 
+/*
+ * 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",
+					  fmtQualifiedDumpable(cekinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN ENCRYPTION KEY %s WITH VALUES ",
+					  fmtQualifiedDumpable(cekinfo));
+
+	for (int i = 0; i < cekinfo->numdata; i++)
+	{
+		appendPQExpBuffer(query, "(");
+
+		appendPQExpBuffer(query, "column_master_key = %s, ", fmtQualifiedDumpable(cekinfo->cekcmks[i]));
+		appendPQExpBuffer(query, "algorithm = '%s', ", get_cmkalg_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,
+								  .namespace = cekinfo->dobj.namespace->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,
+					cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN ENCRYPTION KEY", qcekname,
+					 cekinfo->dobj.namespace->dobj.name, cekinfo->rolname,
+					 cekinfo->dobj.catId, 0, cekinfo->dobj.dumpId);
+
+	if (cekinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cekinfo->dobj.dumpId, InvalidDumpId, "COLUMN ENCRYPTION KEY",
+				qcekname, NULL, cekinfo->dobj.namespace->dobj.name,
+				NULL, cekinfo->rolname, &cekinfo->dacl);
+
+	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",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	appendPQExpBuffer(query, "CREATE COLUMN MASTER KEY %s WITH (",
+					  fmtQualifiedDumpable(cmkinfo));
+
+	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,
+								  .namespace = cmkinfo->dobj.namespace->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,
+					cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_SECLABEL)
+		dumpSecLabel(fout, "COLUMN MASTER KEY", qcmkname,
+					 cmkinfo->dobj.namespace->dobj.name, cmkinfo->rolname,
+					 cmkinfo->dobj.catId, 0, cmkinfo->dobj.dumpId);
+
+	if (cmkinfo->dobj.dump & DUMP_COMPONENT_ACL)
+		dumpACL(fout, cmkinfo->dobj.dumpId, InvalidDumpId, "COLUMN MASTER KEY",
+				qcmkname, NULL, cmkinfo->dobj.namespace->dobj.name,
+				NULL, cmkinfo->rolname, &cmkinfo->dacl);
+
+	destroyPQExpBuffer(delq);
+	destroyPQExpBuffer(query);
+	free(qcmkname);
+}
+
 /*
  * dumpConversion
  *	  write out a single conversion definition
@@ -16193,6 +16540,23 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 										  tbinfo->atttypnames[j]);
 					}
 
+					if (tbinfo->attcek[j])
+					{
+						appendPQExpBuffer(q, " ENCRYPTED WITH (column_encryption_key = %s, ",
+										  fmtQualifiedDumpable(tbinfo->attcek[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_cekalg_name(tbinfo->attencalg[j]));
+					}
+
 					if (print_default)
 					{
 						if (tbinfo->attgenerated[j] == ATTRIBUTE_GENERATED_STORED)
@@ -18702,6 +19066,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 2a7c5873a0a..f451281dc74 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,
@@ -338,6 +340,9 @@ typedef struct _tableInfo
 	bool	   *attisdropped;	/* true if attr is dropped; don't dump it */
 	char	   *attidentity;
 	char	   *attgenerated;
+	struct _CekInfo **attcek;
+	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 */
@@ -698,6 +703,32 @@ typedef struct _SubRelInfo
 	char	   *srsublsn;
 } SubRelInfo;
 
+/*
+ * The CekInfo struct is used to represent column encryption key.
+ */
+typedef struct _CekInfo
+{
+	DumpableObject dobj;
+	DumpableAcl dacl;
+	const char *rolname;
+	int			numdata;
+	/* The following are arrays of numdata entries each: */
+	struct _CmkInfo **cekcmks;
+	int		   *cekcmkalgs;
+	char	  **cekencvals;
+} CekInfo;
+
+/*
+ * The CmkInfo struct is used to represent column master key.
+ */
+typedef struct _CmkInfo
+{
+	DumpableObject dobj;
+	DumpableAcl dacl;
+	const char *rolname;
+	char	   *cmkrealm;
+} CmkInfo;
+
 /*
  *	common utility functions
  */
@@ -719,6 +750,8 @@ extern TableInfo *findTableByOid(Oid oid);
 extern TypeInfo *findTypeByOid(Oid oid);
 extern FuncInfo *findFuncByOid(Oid oid);
 extern OprInfo *findOprByOid(Oid oid);
+extern CekInfo *findCekByOid(Oid oid);
+extern CmkInfo *findCmkByOid(Oid oid);
 extern CollInfo *findCollationByOid(Oid oid);
 extern NamespaceInfo *findNamespaceByOid(Oid oid);
 extern ExtensionInfo *findExtensionByOid(Oid oid);
@@ -747,6 +780,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 7362f7c961a..17d7fdd74d5 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -71,6 +71,8 @@ enum dbObjectTypePriorities
 	PRIO_TSTEMPLATE,
 	PRIO_TSDICT,
 	PRIO_TSCONFIG,
+	PRIO_CMK,
+	PRIO_CEK,
 	PRIO_FDW,
 	PRIO_FOREIGN_SERVER,
 	PRIO_TABLE,
@@ -114,6 +116,8 @@ static const int dbObjectTypePriority[] =
 	[DO_ACCESS_METHOD] = PRIO_ACCESS_METHOD,
 	[DO_OPCLASS] = PRIO_OPFAMILY,
 	[DO_OPFAMILY] = PRIO_OPFAMILY,
+	[DO_CEK] = PRIO_CEK,
+	[DO_CMK] = PRIO_CMK,
 	[DO_COLLATION] = PRIO_COLLATION,
 	[DO_CONVERSION] = PRIO_CONVERSION,
 	[DO_TABLE] = PRIO_TABLE,
@@ -1299,6 +1303,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 73337f33923..84f623b2c9c 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},
@@ -429,6 +431,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)
@@ -654,6 +658,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 0c057fef947..04f94f73489 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -810,6 +810,29 @@
 		unlike => { no_owner => 1, },
 	},
 
+	'ALTER COLUMN ENCRYPTION KEY cek1 OWNER TO' => {
+		regexp =>
+		  qr/^ALTER COLUMN ENCRYPTION KEY dump_test.cek1 OWNER TO .+;/m,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_owner => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
+	'ALTER COLUMN MASTER KEY cmk1 OWNER TO' => {
+		regexp => qr/^ALTER COLUMN MASTER KEY dump_test.cmk1 OWNER TO .+;/m,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_owner => 1,
+			only_dump_measurement => 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, },
@@ -1516,6 +1539,34 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'COMMENT ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 55,
+		create_sql => 'COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1
+					   IS \'comment on column encryption key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN ENCRYPTION KEY dump_test.cek1 IS 'comment on column encryption key';/m,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
+	'COMMENT ON COLUMN MASTER KEY cmk1' => {
+		create_order => 55,
+		create_sql => 'COMMENT ON COLUMN MASTER KEY dump_test.cmk1
+					   IS \'comment on column master key\';',
+		regexp =>
+		  qr/^COMMENT ON COLUMN MASTER KEY dump_test.cmk1 IS 'comment on column master key';/m,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
 	'COMMENT ON LARGE OBJECT ...' => {
 		create_order => 65,
 		create_sql => 'DO $$
@@ -1993,6 +2044,32 @@
 		like => { %full_runs, section_pre_data => 1, },
 	},
 
+	'CREATE COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 51,
+		create_sql =>
+		  "CREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = '\\xDEADBEEF');",
+		regexp => qr/^
+			\QCREATE COLUMN ENCRYPTION KEY dump_test.cek1 WITH VALUES (column_master_key = dump_test.cmk1, algorithm = 'RSAES_OAEP_SHA_1', encrypted_value = \E
+			/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike =>
+		  { exclude_dump_test_schema => 1, only_dump_measurement => 1, },
+	},
+
+	'CREATE COLUMN MASTER KEY cmk1' => {
+		create_order => 50,
+		create_sql =>
+		  "CREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');",
+		regexp => qr/^
+			\QCREATE COLUMN MASTER KEY dump_test.cmk1 WITH (realm = 'myrealm');\E
+			/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike =>
+		  { exclude_dump_test_schema => 1, only_dump_measurement => 1, },
+	},
+
 	'CREATE DATABASE postgres' => {
 		regexp => qr/^
 			\QCREATE DATABASE postgres WITH TEMPLATE = template0 \E
@@ -4101,6 +4178,38 @@
 		unlike => { no_privs => 1, },
 	},
 
+	'GRANT USAGE ON COLUMN ENCRYPTION KEY cek1' => {
+		create_order => 85,
+		create_sql =>
+		  'GRANT USAGE ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN ENCRYPTION KEY dump_test.cek1 TO regress_dump_test_role;\E
+			/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_privs => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
+	'GRANT USAGE ON COLUMN MASTER KEY cmk1' => {
+		create_order => 85,
+		create_sql =>
+		  'GRANT USAGE ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;',
+		regexp => qr/^
+			\QGRANT ALL ON COLUMN MASTER KEY dump_test.cmk1 TO regress_dump_test_role;\E
+			/xm,
+		like =>
+		  { %full_runs, %dump_test_schema_runs, section_pre_data => 1, },
+		unlike => {
+			exclude_dump_test_schema => 1,
+			no_privs => 1,
+			only_dump_measurement => 1,
+		},
+	},
+
 	'GRANT USAGE ON FOREIGN DATA WRAPPER dummy' => {
 		create_order => 85,
 		create_sql => 'GRANT USAGE ON FOREIGN DATA WRAPPER dummy
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 288c1a8c935..125d7cea8c4 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -818,7 +818,11 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd)
 				success = describeTablespaces(pattern, show_verbose);
 				break;
 			case 'c':
-				if (strncmp(cmd, "dconfig", 7) == 0)
+				if (strncmp(cmd, "dcek", 4) == 0)
+					success = listCEKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dcmk", 4) == 0)
+					success = listCMKs(pattern, show_verbose);
+				else if (strncmp(cmd, "dconfig", 7) == 0)
 					success = describeConfigurationParameters(pattern,
 															  show_verbose,
 															  show_system);
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 6433497bcd2..bd7ab861553 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -1538,7 +1538,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;
@@ -1554,6 +1554,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;
@@ -1576,6 +1577,8 @@ describeOneTableDetails(const char *schemaname,
 		char	   *relam;
 	}			tableinfo;
 	bool		show_column_details = false;
+	const char *attusertypid;
+	const char *attusertypmod;
 
 	myopt.default_footer = false;
 	/* This output looks confusing in expanded mode. */
@@ -1852,7 +1855,17 @@ 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)");
+	if (pset.sversion >= 170000)
+	{
+		attusertypid = "CASE WHEN a.attusertypid IS NOT NULL THEN a.attusertypid ELSE a.atttypid END";
+		attusertypmod = "CASE WHEN a.attusertypid IS NOT NULL THEN a.attusertypmod ELSE a.atttypmod END";
+	}
+	else
+	{
+		attusertypid = "a.atttypid";
+		attusertypmod = "a.atttypmod";
+	}
+	appendPQExpBuffer(&buf, ",\n  pg_catalog.format_type(%s, %s)", attusertypid, attusertypmod);
 	atttype_col = cols++;
 
 	if (show_column_details)
@@ -1865,7 +1878,8 @@ describeOneTableDetails(const char *schemaname,
 							 ",\n  a.attnotnull");
 		attrdef_col = cols++;
 		attnotnull_col = cols++;
-		appendPQExpBufferStr(&buf, ",\n  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t\n"
+		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");
 		attcoll_col = cols++;
 		if (pset.sversion >= 100000)
@@ -1917,6 +1931,18 @@ describeOneTableDetails(const char *schemaname,
 			attcompression_col = cols++;
 		}
 
+		/* encryption info */
+		if (pset.sversion >= 170000 &&
+			!pset.hide_column_encryption &&
+			(tableinfo.relkind == RELKIND_RELATION ||
+			 tableinfo.relkind == RELKIND_VIEW ||
+			 tableinfo.relkind == RELKIND_MATVIEW ||
+			 tableinfo.relkind == RELKIND_PARTITIONED_TABLE))
+		{
+			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 ||
@@ -2040,6 +2066,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)
@@ -2132,6 +2160,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),
@@ -4589,6 +4628,152 @@ listConversions(const char *pattern, bool verbose, bool showSystem)
 	return true;
 }
 
+/*
+ * \dcek
+ *
+ * Lists column encryption keys.
+ */
+bool
+listCEKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 170000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cekname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cekowner) AS \"%s\", "
+					  "cmkname AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Master key"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cekacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cek.oid, 'pg_colenckey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colenckey cek "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cek.ceknamespace "
+						 "JOIN pg_catalog.pg_colenckeydata ckd ON (cek.oid = ckd.ckdcekid) "
+						 "JOIN pg_catalog.pg_colmasterkey cmk ON (ckd.ckdcmkid = cmk.oid) ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cekname", NULL,
+								"pg_catalog.pg_cek_is_visible(cek.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2, 4");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column encryption keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
+/*
+ * \dcmk
+ *
+ * Lists column master keys.
+ */
+bool
+listCMKs(const char *pattern, bool verbose)
+{
+	PQExpBufferData buf;
+	PGresult   *res;
+	printQueryOpt myopt = pset.popt;
+
+	if (pset.sversion < 170000)
+	{
+		char		sverbuf[32];
+
+		pg_log_error("The server (version %s) does not support column encryption.",
+					 formatPGVersionNumber(pset.sversion, false,
+										   sverbuf, sizeof(sverbuf)));
+		return true;
+	}
+
+	initPQExpBuffer(&buf);
+
+	printfPQExpBuffer(&buf,
+					  "SELECT "
+					  "n.nspname AS \"%s\", "
+					  "cmkname AS \"%s\", "
+					  "pg_catalog.pg_get_userbyid(cmkowner) AS \"%s\", "
+					  "cmkrealm AS \"%s\"",
+					  gettext_noop("Schema"),
+					  gettext_noop("Name"),
+					  gettext_noop("Owner"),
+					  gettext_noop("Realm"));
+	if (verbose)
+	{
+		appendPQExpBuffer(&buf, ", ");
+		printACLColumn(&buf, "cmkacl");
+		appendPQExpBuffer(&buf,
+						  ", pg_catalog.obj_description(cmk.oid, 'pg_colmasterkey') AS \"%s\"",
+						  gettext_noop("Description"));
+	}
+	appendPQExpBufferStr(&buf,
+						 "\nFROM pg_catalog.pg_colmasterkey cmk "
+						 "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = cmk.cmknamespace ");
+
+	if (!validateSQLNamePattern(&buf, pattern, false, false,
+								"n.nspname", "cmkname", NULL,
+								"pg_catalog.pg_cmk_is_visible(cmk.oid)",
+								NULL, 3))
+	{
+		termPQExpBuffer(&buf);
+		return false;
+	}
+
+	appendPQExpBufferStr(&buf, "ORDER BY 1, 2");
+
+	res = PSQLexec(buf.data);
+	termPQExpBuffer(&buf);
+	if (!res)
+		return false;
+
+	myopt.nullPrint = NULL;
+	myopt.title = _("List of column master keys");
+	myopt.translate_header = true;
+
+	printQuery(res, &myopt, pset.queryFout, false, pset.logfile);
+
+	PQclear(res);
+	return true;
+}
+
 /*
  * \dconfig
  *
diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h
index 273f974538e..8fbab5b361f 100644
--- a/src/bin/psql/describe.h
+++ b/src/bin/psql/describe.h
@@ -79,6 +79,12 @@ extern bool listDomains(const char *pattern, bool verbose, bool showSystem);
 /* \dc */
 extern bool listConversions(const char *pattern, bool verbose, bool showSystem);
 
+/* \dcek */
+extern bool listCEKs(const char *pattern, bool verbose);
+
+/* \dcmk */
+extern bool listCMKs(const char *pattern, bool verbose);
+
 /* \dconfig */
 extern bool describeConfigurationParameters(const char *pattern, bool verbose,
 											bool showSystem);
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 4e79a819d87..f76b1bb5d0b 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -229,6 +229,8 @@ slashUsage(unsigned short int pager)
 	HELP0("  \\dAp[+] [AMPTRN [OPFPTRN]]   list support functions of operator families\n");
 	HELP0("  \\db[+]  [PATTERN]      list tablespaces\n");
 	HELP0("  \\dc[S+] [PATTERN]      list conversions\n");
+	HELP0("  \\dcek[+] [PATTERN]     list column encryption keys\n");
+	HELP0("  \\dcmk[+] [PATTERN]     list column master keys\n");
 	HELP0("  \\dconfig[+] [PATTERN]  list configuration parameters\n");
 	HELP0("  \\dC[+]  [PATTERN]      list casts\n");
 	HELP0("  \\dd[S]  [PATTERN]      show object descriptions not displayed elsewhere\n");
@@ -391,6 +393,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 505f99d8e47..f2207220635 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -138,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 036caaec2ff..92ed10de189 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 6fee3160f02..d5b5a9b4366 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -934,6 +934,20 @@ static const SchemaQuery Query_for_list_of_collations = {
 	.result = "c.collname",
 };
 
+static const SchemaQuery Query_for_list_of_ceks = {
+	.catname = "pg_catalog.pg_colenckey c",
+	.viscondition = "pg_catalog.pg_cek_is_visible(c.oid)",
+	.namespace = "c.ceknamespace",
+	.result = "c.cekname",
+};
+
+static const SchemaQuery Query_for_list_of_cmks = {
+	.catname = "pg_catalog.pg_colmasterkey c",
+	.viscondition = "pg_catalog.pg_cmk_is_visible(c.oid)",
+	.namespace = "c.cmknamespace",
+	.result = "c.cmkname",
+};
+
 static const SchemaQuery Query_for_partition_of_table = {
 	.catname = "pg_catalog.pg_class c1, pg_catalog.pg_class c2, pg_catalog.pg_inherits i",
 	.selcondition = "c1.oid=i.inhparent and i.inhrelid=c2.oid and c2.relispartition",
@@ -1228,6 +1242,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
@@ -1717,7 +1733,7 @@ psql_completion(const char *text, int start, int end)
 		"\\connect", "\\conninfo", "\\C", "\\cd", "\\copy",
 		"\\copyright", "\\crosstabview",
 		"\\d", "\\da", "\\dA", "\\dAc", "\\dAf", "\\dAo", "\\dAp",
-		"\\db", "\\dc", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
+		"\\db", "\\dc", "\\dcek", "\\dcmk", "\\dconfig", "\\dC", "\\dd", "\\ddp", "\\dD",
 		"\\des", "\\det", "\\deu", "\\dew", "\\dE", "\\df",
 		"\\dF", "\\dFd", "\\dFp", "\\dFt", "\\dg", "\\di", "\\dl", "\\dL",
 		"\\dm", "\\dn", "\\do", "\\dO", "\\dp", "\\dP", "\\dPi", "\\dPt",
@@ -1974,6 +1990,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_SCHEMA_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", "SET SCHEMA");
+
+	/* ALTER/DROP COLUMN MASTER KEY */
+	else if (Matches("ALTER|DROP", "COLUMN", "MASTER", "KEY"))
+		COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_cmks);
+
+	/* ALTER COLUMN MASTER KEY */
+	else if (Matches("ALTER", "COLUMN", "MASTER", "KEY", MatchAny))
+		COMPLETE_WITH("(", "OWNER TO", "RENAME TO", "SET SCHEMA");
+
 	/* ALTER CONVERSION <name> */
 	else if (Matches("ALTER", "CONVERSION", MatchAny))
 		COMPLETE_WITH("OWNER TO", "RENAME TO", "SET SCHEMA");
@@ -2949,6 +2981,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",
@@ -3708,7 +3760,7 @@ psql_completion(const char *text, int start, int end)
 
 /* DISCARD */
 	else if (Matches("DISCARD"))
-		COMPLETE_WITH("ALL", "PLANS", "SEQUENCES", "TEMP");
+		COMPLETE_WITH("ALL", "COLUMN ENCRYPTION KEYS", "PLANS", "SEQUENCES", "TEMP");
 
 /* DO */
 	else if (Matches("DO"))
@@ -3722,6 +3774,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) ||
@@ -4043,6 +4096,8 @@ psql_completion(const char *text, int start, int end)
 											"ALL ROUTINES IN SCHEMA",
 											"ALL SEQUENCES IN SCHEMA",
 											"ALL TABLES IN SCHEMA",
+											"COLUMN ENCRYPTION KEY",
+											"COLUMN MASTER KEY",
 											"DATABASE",
 											"DOMAIN",
 											"FOREIGN DATA WRAPPER",
@@ -4161,6 +4216,19 @@ psql_completion(const char *text, int start, int end)
 			COMPLETE_WITH("FROM");
 	}
 
+	/*
+	 * Complete "GRANT/REVOKE * ON COLUMN ENCRYPTION|MASTER KEY *" with
+	 * TO/FROM
+	 */
+	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny) ||
+			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "COLUMN", "ENCRYPTION|MASTER", "KEY", MatchAny))
+	{
+		if (TailMatches("GRANT", MatchAny, MatchAny, MatchAny, MatchAny, MatchAny, MatchAny))
+			COMPLETE_WITH("TO");
+		else
+			COMPLETE_WITH("FROM");
+	}
+
 	/* Complete "GRANT/REVOKE * ON FOREIGN DATA WRAPPER *" with TO/FROM */
 	else if (TailMatches("GRANT|REVOKE", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny) ||
 			 TailMatches("REVOKE", "GRANT", "OPTION", "FOR", MatchAny, "ON", "FOREIGN", "DATA", "WRAPPER", MatchAny))
diff --git a/src/common/Makefile b/src/common/Makefile
index 3d83299432b..448a19d6845 100644
--- a/src/common/Makefile
+++ b/src/common/Makefile
@@ -49,6 +49,7 @@ OBJS_COMMON = \
 	binaryheap.o \
 	blkreftable.o \
 	checksum_helper.o \
+	colenc.o \
 	compression.o \
 	config_info.o \
 	controldata_utils.o \
diff --git a/src/common/colenc.c b/src/common/colenc.c
new file mode 100644
index 00000000000..b3ba84087ef
--- /dev/null
+++ b/src/common/colenc.c
@@ -0,0 +1,107 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.c
+ *
+ * Shared code for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/common/colenc.c
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef FRONTEND
+#include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
+
+#include "common/colenc.h"
+
+int
+get_cmkalg_num(const char *name)
+{
+	if (strcmp(name, "unspecified") == 0)
+		return PG_CMK_UNSPECIFIED;
+	else if (strcmp(name, "RSAES_OAEP_SHA_1") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_1;
+	else if (strcmp(name, "RSAES_OAEP_SHA_256") == 0)
+		return PG_CMK_RSAES_OAEP_SHA_256;
+	else
+		return 0;
+}
+
+const char *
+get_cmkalg_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return "unspecified";
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSAES_OAEP_SHA_1";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSAES_OAEP_SHA_256";
+	}
+
+	return NULL;
+}
+
+/*
+ * JSON Web Algorithms (JWA) names (RFC 7518)
+ *
+ * This is useful for some key management systems that use these names
+ * natively.
+ *
+ * This only supports algorithms that have a mapping in JWA.  For any other
+ * ones, it returns NULL.
+ */
+const char *
+get_cmkalg_jwa_name(int num)
+{
+	switch (num)
+	{
+		case PG_CMK_UNSPECIFIED:
+			return NULL;
+		case PG_CMK_RSAES_OAEP_SHA_1:
+			return "RSA-OAEP";
+		case PG_CMK_RSAES_OAEP_SHA_256:
+			return "RSA-OAEP-256";
+	}
+
+	return NULL;
+}
+
+int
+get_cekalg_num(const char *name)
+{
+	if (strcmp(name, "AEAD_AES_128_CBC_HMAC_SHA_256") == 0)
+		return PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256;
+	else if (strcmp(name, "AEAD_AES_192_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_384") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384;
+	else if (strcmp(name, "AEAD_AES_256_CBC_HMAC_SHA_512") == 0)
+		return PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512;
+	else
+		return 0;
+}
+
+const char *
+get_cekalg_name(int num)
+{
+	switch (num)
+	{
+		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";
+	}
+
+	return NULL;
+}
diff --git a/src/common/meson.build b/src/common/meson.build
index de68e408fa3..9e88f31c061 100644
--- a/src/common/meson.build
+++ b/src/common/meson.build
@@ -6,6 +6,7 @@ common_sources = files(
   'binaryheap.c',
   'blkreftable.c',
   'checksum_helper.c',
+  'colenc.c',
   'compression.c',
   'controldata_utils.c',
   'encnames.c',
diff --git a/src/include/access/printtup.h b/src/include/access/printtup.h
index b1fecf873b4..13e38ba5f3a 100644
--- a/src/include/access/printtup.h
+++ b/src/include/access/printtup.h
@@ -20,9 +20,13 @@ 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);
 
+extern void DiscardColumnEncryptionKeys(void);
+
 extern void debugStartup(DestReceiver *self, int operation,
 						 TupleDesc typeinfo);
 extern bool debugtup(TupleTableSlot *slot, DestReceiver *self);
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 167f91a6e3f..9cf93c8735d 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -81,7 +81,10 @@ CATALOG_HEADERS := \
 	pg_publication_namespace.h \
 	pg_publication_rel.h \
 	pg_subscription.h \
-	pg_subscription_rel.h
+	pg_subscription_rel.h \
+	pg_colmasterkey.h \
+	pg_colenckey.h \
+	pg_colenckeydata.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
 
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index 21e31f9c974..cfe383799c1 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -23,6 +23,7 @@
 #define CHKATYPE_ANYARRAY		0x01	/* allow ANYARRAY */
 #define CHKATYPE_ANYRECORD		0x02	/* allow RECORD and RECORD[] */
 #define CHKATYPE_IS_PARTKEY		0x04	/* attname is part key # not column */
+#define CHKATYPE_ENCRYPTED		0x08	/* allow internal encrypted types */
 
 typedef struct RawColumnDefault
 {
@@ -72,6 +73,7 @@ extern Oid	heap_create_with_catalog(const char *relname,
 									 Oid ownerid,
 									 Oid accessmtd,
 									 TupleDesc tupdesc,
+									 const FormExtraData_pg_attribute tupdesc_extra[],
 									 List *cooked_constraints,
 									 char relkind,
 									 char relpersistence,
@@ -140,7 +142,7 @@ extern const FormData_pg_attribute *SystemAttributeDefinition(AttrNumber attno);
 
 extern const FormData_pg_attribute *SystemAttributeByName(const char *attname);
 
-extern void CheckAttributeNamesTypes(TupleDesc tupdesc, char relkind,
+extern void CheckAttributeNamesTypes(TupleDesc tupdesc, const FormExtraData_pg_attribute tupdesc_extra[], char relkind,
 									 int flags);
 
 extern void CheckAttributeType(const char *attname,
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index f70d1daba52..a5af2a86bf9 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -69,6 +69,9 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.h',
+  'pg_colmasterkey.h',
+  'pg_colenckey.h',
+  'pg_colenckeydata.h',
 ]
 
 # The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 8d434d48d57..553780b6ff3 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -116,6 +116,12 @@ extern bool OpclassIsVisible(Oid opcid);
 extern Oid	OpfamilynameGetOpfid(Oid amid, const char *opfname);
 extern bool OpfamilyIsVisible(Oid opfid);
 
+extern Oid	get_cek_oid(List *names, bool missing_ok);
+extern bool CEKIsVisible(Oid cekid);
+
+extern Oid	get_cmk_oid(List *names, bool missing_ok);
+extern bool CMKIsVisible(Oid cmkid);
+
 extern Oid	CollationGetCollid(const char *collname);
 extern bool CollationIsVisible(Oid collid);
 
diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat
index d8a05214b11..a10077d1f13 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 352558c1f06..b004bba7620 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -400,6 +400,12 @@
 { 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 1c62b8bfcb5..3cc32452820 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -184,6 +184,15 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	/* Column-level FDW options */
 	text		attfdwoptions[1] BKI_DEFAULT(_null_);
 
+	/* column encryption key */
+	Oid			attcek BKI_DEFAULT(_null_) BKI_FORCE_NULL BKI_LOOKUP_OPT(pg_colenckey);
+
+	/*
+	 * User-visible type and typmod, currently used for encrypted columns.
+	 */
+	Oid			attusertypid BKI_DEFAULT(_null_) BKI_FORCE_NULL BKI_LOOKUP_OPT(pg_type);
+	int32		attusertypmod BKI_DEFAULT(_null_) BKI_FORCE_NULL;
+
 	/*
 	 * Missing value for added columns. This is a one element array which lets
 	 * us store a value of the attribute type here.
@@ -199,7 +208,7 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
  * can access the variable-length fields except in a real tuple!
  */
 #define ATTRIBUTE_FIXED_PART_SIZE \
-	(offsetof(FormData_pg_attribute,attcollation) + sizeof(Oid))
+	(offsetof(FormData_pg_attribute,attcollation) + sizeof(int32))
 
 /* ----------------
  *		Form_pg_attribute corresponds to a pointer to a tuple with
@@ -220,6 +229,9 @@ typedef struct FormExtraData_pg_attribute
 {
 	NullableDatum attstattarget;
 	NullableDatum attoptions;
+	NullableDatum attcek;
+	NullableDatum attusertypid;
+	NullableDatum attusertypmod;
 } FormExtraData_pg_attribute;
 
 DECLARE_UNIQUE_INDEX(pg_attribute_relid_attnam_index, 2658, AttributeRelidNameIndexId, pg_attribute, btree(attrelid oid_ops, attname name_ops));
diff --git a/src/include/catalog/pg_colenckey.h b/src/include/catalog/pg_colenckey.h
new file mode 100644
index 00000000000..c69d245109e
--- /dev/null
+++ b/src/include/catalog/pg_colenckey.h
@@ -0,0 +1,49 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckey.h
+ *	  definition of the "column encryption key" system catalog
+ *
+ * Portions Copyright (c) 1996-2024, 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			ceknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cekowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	aclitem		cekacl[1] BKI_DEFAULT(_null_);
+#endif
+} FormData_pg_colenckey;
+
+typedef FormData_pg_colenckey *Form_pg_colenckey;
+
+DECLARE_TOAST(pg_colenckey, 8263, 8264);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_colenckey_oid_index, 8240, ColumnEncKeyOidIndexId, pg_colenckey, btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckey_cekname_nsp_index, 8242, ColumnEncKeyNameNspIndexId, pg_colenckey, btree(cekname name_ops, ceknamespace oid_ops));
+
+MAKE_SYSCACHE(CEKOID, pg_colenckey_oid_index, 8);
+MAKE_SYSCACHE(CEKNAMENSP, pg_colenckey_cekname_nsp_index, 8);
+
+#endif
diff --git a/src/include/catalog/pg_colenckeydata.h b/src/include/catalog/pg_colenckeydata.h
new file mode 100644
index 00000000000..e6ce40ad481
--- /dev/null
+++ b/src/include/catalog/pg_colenckeydata.h
@@ -0,0 +1,49 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colenckeydata.h
+ *	  definition of the "column encryption key data" system catalog
+ *
+ * Portions Copyright (c) 1996-2024, 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);
+	int32		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, pg_colenckeydata, btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colenckeydata_ckdcekid_ckdcmkid_index, 8252, ColumnEncKeyCekidCmkidIndexId, pg_colenckeydata, btree(ckdcekid oid_ops, ckdcmkid oid_ops));
+
+MAKE_SYSCACHE(CEKDATAOID, pg_colenckeydata_oid_index, 8);
+MAKE_SYSCACHE(CEKDATACEKCMK, pg_colenckeydata_ckdcekid_ckdcmkid_index, 8);
+
+#endif
diff --git a/src/include/catalog/pg_colmasterkey.h b/src/include/catalog/pg_colmasterkey.h
new file mode 100644
index 00000000000..1629760a9b9
--- /dev/null
+++ b/src/include/catalog/pg_colmasterkey.h
@@ -0,0 +1,50 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_colmasterkey.h
+ *	  definition of the "column master key" system catalog
+ *
+ * Portions Copyright (c) 1996-2024, 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			cmknamespace BKI_DEFAULT(pg_catalog) BKI_LOOKUP(pg_namespace);
+	Oid			cmkowner BKI_LOOKUP(pg_authid);
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	text		cmkrealm BKI_FORCE_NOT_NULL;
+	aclitem		cmkacl[1] BKI_DEFAULT(_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, pg_colmasterkey, btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_colmasterkey_cmkname_nsp_index, 8241, ColumnMasterKeyNameNspIndexId, pg_colmasterkey, btree(cmkname name_ops, cmknamespace oid_ops));
+
+MAKE_SYSCACHE(CMKOID, pg_colmasterkey_oid_index, 8);
+MAKE_SYSCACHE(CMKNAMENSP, pg_colmasterkey_cmkname_nsp_index, 8);
+
+#endif
diff --git a/src/include/catalog/pg_opclass.dat b/src/include/catalog/pg_opclass.dat
index 6c30770fe7c..c3cc8fd919a 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 0e7511dde1c..a0b135cf19e 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -3458,4 +3458,19 @@
   oprcode => 'multirange_after_multirange', oprrest => 'multirangesel',
   oprjoin => 'scalargtjoinsel' },
 
+{ oid => '8247', descr => 'equal',
+  oprname => '=', 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 c8ac8c73def..9b7308592fe 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 153d816a053..19d87c56194 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6439,6 +6439,12 @@
   proname => 'pg_collation_is_visible', procost => '10', provolatile => 's',
   prorettype => 'bool', proargtypes => 'oid',
   prosrc => 'pg_collation_is_visible' },
+{ oid => '8261', descr => 'is column encryption key visible in search path?',
+  proname => 'pg_cek_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_cek_is_visible' },
+{ oid => '8262', descr => 'is column master key visible in search path?',
+  proname => 'pg_cmk_is_visible', procost => '10', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid', prosrc => 'pg_cmk_is_visible' },
 
 { oid => '2854', descr => 'get OID of current session\'s temp schema, if any',
   proname => 'pg_my_temp_schema', provolatile => 's', proparallel => 'r',
@@ -7226,6 +7232,68 @@
   proname => 'fmgr_sql_validator', provolatile => 's', prorettype => 'void',
   proargtypes => 'oid', prosrc => 'fmgr_sql_validator' },
 
+{ oid => '8265',
+  descr => 'user privilege on column encryption key by username, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_encryption_key_privilege_name_name' },
+{ oid => '8266',
+  descr => 'user privilege on column encryption key by username, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_encryption_key_privilege_name_id' },
+{ oid => '8267',
+  descr => 'user privilege on column encryption key by user oid, column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_encryption_key_privilege_id_name' },
+{ oid => '8268',
+  descr => 'user privilege on column encryption key by user oid, column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_encryption_key_privilege_id_id' },
+{ oid => '8269',
+  descr => 'current user privilege on column encryption key by column encryption key name',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_encryption_key_privilege_name' },
+{ oid => '8270',
+  descr => 'current user privilege on column encryption key by column encryption key oid',
+  proname => 'has_column_encryption_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_encryption_key_privilege_id' },
+
+{ oid => '8271',
+  descr => 'user privilege on column master key by username, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name text text',
+  prosrc => 'has_column_master_key_privilege_name_name' },
+{ oid => '8272',
+  descr => 'user privilege on column master key by username, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'name oid text',
+  prosrc => 'has_column_master_key_privilege_name_id' },
+{ oid => '8273',
+  descr => 'user privilege on column master key by user oid, column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text text',
+  prosrc => 'has_column_master_key_privilege_id_name' },
+{ oid => '8274',
+  descr => 'user privilege on column master key by user oid, column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid oid text',
+  prosrc => 'has_column_master_key_privilege_id_id' },
+{ oid => '8275',
+  descr => 'current user privilege on column master key by column master key name',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'text text',
+  prosrc => 'has_column_master_key_privilege_name' },
+{ oid => '8276',
+  descr => 'current user privilege on column master key by column master key oid',
+  proname => 'has_column_master_key_privilege', provolatile => 's',
+  prorettype => 'bool', proargtypes => 'oid text',
+  prosrc => 'has_column_master_key_privilege_id' },
+
 { oid => '2250',
   descr => 'user privilege on database by username, database name',
   proname => 'has_database_privilege', provolatile => 's', prorettype => 'bool',
@@ -12200,4 +12268,36 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+{ oid => '8253', descr => 'I/O',
+  proname => 'pg_encrypted_det_in', prorettype => 'pg_encrypted_det',
+  proargtypes => 'cstring', prosrc => 'pg_encrypted_in' },
+{ oid => '8254', descr => 'I/O',
+  proname => 'pg_encrypted_det_out', prorettype => 'cstring',
+  proargtypes => 'pg_encrypted_det', prosrc => 'pg_encrypted_out' },
+{ oid => '8255', descr => 'I/O',
+  proname => 'pg_encrypted_det_recv', prorettype => 'pg_encrypted_det',
+  proargtypes => 'internal', prosrc => 'pg_encrypted_recv' },
+{ oid => '8256', descr => 'I/O',
+  proname => 'pg_encrypted_det_send', prorettype => 'bytea',
+  proargtypes => 'pg_encrypted_det', prosrc => 'pg_encrypted_send' },
+
+{ oid => '8257', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_in', prorettype => 'pg_encrypted_rnd',
+  proargtypes => 'cstring', prosrc => 'pg_encrypted_in' },
+{ oid => '8258', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_out', prorettype => 'cstring',
+  proargtypes => 'pg_encrypted_rnd', prosrc => 'pg_encrypted_out' },
+{ oid => '8259', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_recv', prorettype => 'pg_encrypted_rnd',
+  proargtypes => 'internal', prosrc => 'pg_encrypted_recv' },
+{ oid => '8260', descr => 'I/O',
+  proname => 'pg_encrypted_rnd_send', prorettype => 'bytea',
+  proargtypes => 'pg_encrypted_rnd', prosrc => 'pg_encrypted_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 d29194da31f..5f7d5a37e62 100644
--- a/src/include/catalog/pg_type.dat
+++ b/src/include/catalog/pg_type.dat
@@ -694,4 +694,17 @@
   typreceive => 'brin_minmax_multi_summary_recv',
   typsend => 'brin_minmax_multi_summary_send', typalign => 'i',
   typstorage => 'x', typcollation => 'default' },
+
+# Note: typstorage 'e' since compression is not useful for encrypted data
+{ oid => '8243', descr => 'encrypted column (deterministic)',
+  typname => 'pg_encrypted_det', typlen => '-1', typbyval => 'f',
+  typcategory => 'Y', typinput => 'pg_encrypted_det_in',
+  typoutput => 'pg_encrypted_det_out', typreceive => 'pg_encrypted_det_recv',
+  typsend => 'pg_encrypted_det_send', typalign => 'i', typstorage => 'e' },
+{ oid => '8244', descr => 'encrypted column (randomized)',
+  typname => 'pg_encrypted_rnd', typlen => '-1', typbyval => 'f',
+  typcategory => 'Y', typinput => 'pg_encrypted_rnd_in',
+  typoutput => 'pg_encrypted_rnd_out', typreceive => 'pg_encrypted_rnd_recv',
+  typsend => 'pg_encrypted_rnd_send', typalign => 'i', typstorage => 'e' },
+
 ]
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index e9259697321..533b81782c4 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -297,6 +297,7 @@ MAKE_SYSCACHE(TYPENAMENSP, pg_type_typname_nsp_index, 64);
 #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 00000000000..6b881d969d2
--- /dev/null
+++ b/src/include/commands/colenccmds.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenccmds.h
+ *	  prototypes for colenccmds.c.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, 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);
+extern ObjectAddress AlterColumnMasterKey(ParseState *pstate, AlterColumnMasterKeyStmt *stmt);
+
+#endif							/* COLENCCMDS_H */
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 85cbad3d0c2..5169e03cedf 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -27,7 +27,7 @@ struct AlterTableUtilityContext;	/* avoid including tcop/utility.h here */
 extern ObjectAddress DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 									ObjectAddress *typaddress, const char *queryString);
 
-extern TupleDesc BuildDescForRelation(const List *columns);
+extern TupleDesc BuildDescForRelation(const List *columns, FormExtraData_pg_attribute **tupdesc_extra_p);
 
 extern void RemoveRelations(DropStmt *drop);
 
@@ -107,4 +107,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
 extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
 												 List *partConstraint);
 
+extern List *makeColumnEncryption(HeapTuple attrtup);
+
 #endif							/* TABLECMDS_H */
diff --git a/src/include/common/colenc.h b/src/include/common/colenc.h
new file mode 100644
index 00000000000..86a5134164c
--- /dev/null
+++ b/src/include/common/colenc.h
@@ -0,0 +1,51 @@
+/*-------------------------------------------------------------------------
+ *
+ * colenc.h
+ *
+ * Shared definitions for column encryption algorithms.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/include/common/colenc.h
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef COMMON_COLENC_H
+#define COMMON_COLENC_H
+
+/*
+ * Constants for CMK and CEK algorithms.  Note that these are part of the
+ * protocol.  In either case, don't assign zero, so that that can be used as
+ * an invalid value.
+ *
+ * Names should use IANA-style capitalization and punctuation ("LIKE_THIS").
+ *
+ * When making changes, also update protocol.sgml.
+ */
+
+#define PG_CMK_UNSPECIFIED				1
+#define PG_CMK_RSAES_OAEP_SHA_1			2
+#define PG_CMK_RSAES_OAEP_SHA_256		3
+
+/*
+ * These algorithms are part of the RFC 5116 realm of AEAD algorithms (even
+ * though they never became an official IETF standard).  So for propriety, we
+ * use "private use" numbers from
+ * <https://www.iana.org/assignments/aead-parameters/aead-parameters.xhtml>.
+ */
+#define PG_CEK_AEAD_AES_128_CBC_HMAC_SHA_256	32768
+#define PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384	32769
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384	32770
+#define PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512	32771
+
+/*
+ * Functions to convert between names and numbers
+ */
+extern int	get_cmkalg_num(const char *name);
+extern const char *get_cmkalg_name(int num);
+extern const char *get_cmkalg_jwa_name(int num);
+extern int	get_cekalg_num(const char *name);
+extern const char *get_cekalg_name(int num);
+
+#endif
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 05cb1874c58..0f0be990797 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -150,6 +150,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/libpq/protocol.h b/src/include/libpq/protocol.h
index 4b8d4403656..a2db4547767 100644
--- a/src/include/libpq/protocol.h
+++ b/src/include/libpq/protocol.h
@@ -57,6 +57,8 @@
 #define PqMsg_PortalSuspended		's'
 #define PqMsg_ParameterDescription	't'
 #define PqMsg_NegotiateProtocolVersion 'v'
+#define PqMsg_ColumnMasterKey		'y'
+#define PqMsg_ColumnEncryptionKey	'Y'
 
 
 /* These are the codes sent by both the frontend and backend. */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index f763f790b18..01913659bc6 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -726,6 +726,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? */
@@ -762,11 +763,12 @@ typedef enum TableLikeOption
 	CREATE_TABLE_LIKE_COMPRESSION = 1 << 1,
 	CREATE_TABLE_LIKE_CONSTRAINTS = 1 << 2,
 	CREATE_TABLE_LIKE_DEFAULTS = 1 << 3,
-	CREATE_TABLE_LIKE_GENERATED = 1 << 4,
-	CREATE_TABLE_LIKE_IDENTITY = 1 << 5,
-	CREATE_TABLE_LIKE_INDEXES = 1 << 6,
-	CREATE_TABLE_LIKE_STATISTICS = 1 << 7,
-	CREATE_TABLE_LIKE_STORAGE = 1 << 8,
+	CREATE_TABLE_LIKE_ENCRYPTED = 1 << 4,
+	CREATE_TABLE_LIKE_GENERATED = 1 << 5,
+	CREATE_TABLE_LIKE_IDENTITY = 1 << 6,
+	CREATE_TABLE_LIKE_INDEXES = 1 << 7,
+	CREATE_TABLE_LIKE_STATISTICS = 1 << 8,
+	CREATE_TABLE_LIKE_STORAGE = 1 << 9,
 	CREATE_TABLE_LIKE_ALL = PG_INT32_MAX
 } TableLikeOption;
 
@@ -2266,6 +2268,9 @@ typedef enum ObjectType
 	OBJECT_CAST,
 	OBJECT_COLUMN,
 	OBJECT_COLLATION,
+	OBJECT_CEK,
+	OBJECT_CEKDATA,
+	OBJECT_CMK,
 	OBJECT_CONVERSION,
 	OBJECT_DATABASE,
 	OBJECT_DEFAULT,
@@ -2456,6 +2461,31 @@ typedef struct AlterCollationStmt
 } AlterCollationStmt;
 
 
+/* ----------------------
+ * Alter Column Encryption Key
+ * ----------------------
+ */
+typedef struct AlterColumnEncryptionKeyStmt
+{
+	NodeTag		type;
+	List	   *cekname;
+	bool		isDrop;			/* ADD or DROP the items? */
+	List	   *definition;
+} AlterColumnEncryptionKeyStmt;
+
+
+/* ----------------------
+ * Alter Column Master Key
+ * ----------------------
+ */
+typedef struct AlterColumnMasterKeyStmt
+{
+	NodeTag		type;
+	List	   *cmkname;
+	List	   *definition;
+} AlterColumnMasterKeyStmt;
+
+
 /* ----------------------
  *	Alter Domain
  *
@@ -3934,6 +3964,7 @@ typedef struct CheckPointStmt
 typedef enum DiscardMode
 {
 	DISCARD_ALL,
+	DISCARD_COLUMN_ENCRYPTION_KEYS,
 	DISCARD_PLANS,
 	DISCARD_SEQUENCES,
 	DISCARD_TEMP,
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f9a4afd4723..02dc1fc8653 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -152,6 +152,7 @@ PG_KEYWORD("empty", EMPTY_P, UNRESERVED_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("error", ERROR_P, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -269,6 +270,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_param.h b/src/include/parser/parse_param.h
index 6459d4ab6f3..08008b1973f 100644
--- a/src/include/parser/parse_param.h
+++ b/src/include/parser/parse_param.h
@@ -21,5 +21,6 @@ extern void setup_parse_variable_parameters(ParseState *pstate,
 											Oid **paramTypes, int *numParams);
 extern void check_variable_parameters(ParseState *pstate, Query *query);
 extern bool query_contains_extern_params(Query *query);
+extern void find_param_origs(List *query_list, Oid **param_orig_tbls, AttrNumber **param_orig_cols);
 
 #endif							/* PARSE_PARAM_H */
diff --git a/src/include/tcop/cmdtaglist.h b/src/include/tcop/cmdtaglist.h
index 7fdcec6dd93..a4715baf0a0 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)
@@ -130,6 +134,7 @@ PG_CMDTAG(CMDTAG_DECLARE_CURSOR, "DECLARE CURSOR", false, false, false)
 PG_CMDTAG(CMDTAG_DELETE, "DELETE", false, false, true)
 PG_CMDTAG(CMDTAG_DISCARD, "DISCARD", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_ALL, "DISCARD ALL", false, false, false)
+PG_CMDTAG(CMDTAG_DISCARD_COLUMN_ENCRYPTION_KEYS, "DISCARD COLUMN ENCRYPTION KEYS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_PLANS, "DISCARD PLANS", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_SEQUENCES, "DISCARD SEQUENCES", false, false, false)
 PG_CMDTAG(CMDTAG_DISCARD_TEMP, "DISCARD TEMP", false, false, false)
@@ -138,6 +143,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/utils/acl.h b/src/include/utils/acl.h
index 3a0baf30395..152d2b0da90 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -159,6 +159,8 @@ typedef struct ArrayType Acl;
 #define ACL_ALL_RIGHTS_COLUMN		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_REFERENCES)
 #define ACL_ALL_RIGHTS_RELATION		(ACL_INSERT|ACL_SELECT|ACL_UPDATE|ACL_DELETE|ACL_TRUNCATE|ACL_REFERENCES|ACL_TRIGGER|ACL_MAINTAIN)
 #define ACL_ALL_RIGHTS_SEQUENCE		(ACL_USAGE|ACL_SELECT|ACL_UPDATE)
+#define ACL_ALL_RIGHTS_CEK			(ACL_USAGE)
+#define ACL_ALL_RIGHTS_CMK			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_DATABASE		(ACL_CREATE|ACL_CREATE_TEMP|ACL_CONNECT)
 #define ACL_ALL_RIGHTS_FDW			(ACL_USAGE)
 #define ACL_ALL_RIGHTS_FOREIGN_SERVER (ACL_USAGE)
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 35a8dec2b9f..6e512455c35 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -164,6 +164,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);
@@ -203,6 +204,9 @@ 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 char *get_cek_name(Oid cekid, bool missing_ok);
+extern Oid	get_cekdata_oid(Oid cekid, Oid cmkid, 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 a90dfdf9067..d3c37614389 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -209,6 +209,9 @@ extern void CompleteCachedPlan(CachedPlanSource *plansource,
 extern void SaveCachedPlan(CachedPlanSource *plansource);
 extern void DropCachedPlan(CachedPlanSource *plansource);
 
+extern List *RevalidateCachedQuery(CachedPlanSource *plansource,
+								   QueryEnvironment *queryEnv);
+
 extern void CachedPlanSetParentContext(CachedPlanSource *plansource,
 									   MemoryContext newcontext);
 
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile
index fe2af575c5d..5539222b38a 100644
--- a/src/interfaces/libpq/Makefile
+++ b/src/interfaces/libpq/Makefile
@@ -52,6 +52,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 8ee08115100..04a86c09a12 100644
--- a/src/interfaces/libpq/exports.txt
+++ b/src/interfaces/libpq/exports.txt
@@ -204,3 +204,7 @@ PQcancelReset             201
 PQcancelFinish            202
 PQsocketPoll              203
 PQsetChunkedRowsMode      204
+PQexecPreparedDescribed   205
+PQsendQueryPreparedDescribed 206
+PQfisencrypted            207
+PQparamisencrypted        208
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index e35bdc40361..6425c80032c 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -359,6 +359,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)},
+
 	{"load_balance_hosts", "PGLOADBALANCEHOSTS",
 		DefaultLoadBalanceHosts, NULL,
 		"Load-Balance-Hosts", "", 8,	/* sizeof("disable") = 8 */
@@ -1822,6 +1830,28 @@ pqConnectOptions2(PGconn *conn)
 			goto oom_error;
 	}
 
+	/*
+	 * validate column_encryption option
+	 */
+	if (conn->column_encryption_setting)
+	{
+		if (strcmp(conn->column_encryption_setting, "on") == 0 ||
+			strcmp(conn->column_encryption_setting, "true") == 0 ||
+			strcmp(conn->column_encryption_setting, "1") == 0)
+			conn->column_encryption_enabled = true;
+		else if (strcmp(conn->column_encryption_setting, "off") == 0 ||
+				 strcmp(conn->column_encryption_setting, "false") == 0 ||
+				 strcmp(conn->column_encryption_setting, "0") == 0)
+			conn->column_encryption_enabled = false;
+		else
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"column_encryption", conn->column_encryption_setting);
+			return false;
+		}
+	}
+
 	/*
 	 * Only if we get this far is it appropriate to try to connect. (We need a
 	 * state flag, rather than just the boolean result of this function, in
@@ -4679,6 +4709,22 @@ freePGconn(PGconn *conn)
 	free(conn->gsslib);
 	free(conn->gssdelegation);
 	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 00000000000..462535c3832
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt-openssl.c
@@ -0,0 +1,840 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt-openssl.c
+ *
+ * client-side column encryption support using OpenSSL
+ *
+ * Portions Copyright (c) 1996-2024, 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 "common/colenc.h"
+#include "port/pg_bswap.h"
+
+#include <openssl/evp.h>
+
+
+/*
+ * When TEST_ENCRYPT is defined, this file builds a standalone program that
+ * checks encryption test cases against the specification document.
+ *
+ * We have to replace some functions that are not available in that
+ * environment.
+ */
+#ifdef TEST_ENCRYPT
+
+#define libpq_gettext(x) (x)
+#define libpq_append_conn_error(conn, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); exit(1); } while(0)
+#define pqResultAlloc(res, nBytes, isBinary) malloc(nBytes)
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Decrypt the CEK given by "from" and "fromlen" (data typically sent from the
+ * server) using the CMK in cmkfilename.
+ */
+unsigned char *
+decrypt_cek_from_file(PGconn *conn, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	const EVP_MD *md = NULL;
+	EVP_PKEY   *key = NULL;
+	RSA		   *rsa = NULL;
+	BIO		   *bio = 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;
+		case PG_CMK_UNSPECIFIED:
+			libpq_append_conn_error(conn, "unspecified CMK algorithm not supported with file lookup scheme");
+			goto fail;
+		default:
+			libpq_append_conn_error(conn, "unsupported CMK algorithm ID: %d", cmkalg);
+			goto fail;
+	}
+
+	bio = BIO_new_file(cmkfilename, "r");
+	if (!bio)
+	{
+		libpq_append_conn_error(conn, "could not open file \"%s\": %m", cmkfilename);
+		goto fail;
+	}
+
+	rsa = RSA_new();
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not allocate RSA structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/*
+	 * Note: We must go through BIO and not say use PEM_read_RSAPrivateKey()
+	 * directly on a FILE.  Otherwise, we get into "no OPENSSL_Applink" hell
+	 * on Windows (which happens whenever you pass a stdio handle from the
+	 * application into OpenSSL).
+	 */
+	rsa = PEM_read_bio_RSAPrivateKey(bio, &rsa, NULL, NULL);
+	if (!rsa)
+	{
+		libpq_append_conn_error(conn, "could not read RSA private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	key = EVP_PKEY_new();
+	if (!key)
+	{
+		libpq_append_conn_error(conn, "could not allocate private key structure: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_PKEY_assign_RSA(key, rsa))
+	{
+		libpq_append_conn_error(conn, "could not assign private key: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	ctx = EVP_PKEY_CTX_new(key, NULL);
+	if (!ctx)
+	{
+		libpq_append_conn_error(conn, "could not allocate public key algorithm context: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt_init(ctx) <= 0)
+	{
+		libpq_append_conn_error(conn, "decryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "could not set RSA parameter: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	/* get output length */
+	if (EVP_PKEY_decrypt(ctx, NULL, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption setup failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	out = malloc(outlen);
+	if (!out)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+
+	if (EVP_PKEY_decrypt(ctx, out, &outlen, from, fromlen) <= 0)
+	{
+		libpq_append_conn_error(conn, "RSA decryption failed: %s",
+								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);
+	BIO_free(bio);
+
+	return out;
+}
+
+
+/*
+ * The routines below implement the AEAD algorithms specified in
+ * <https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05>
+ * for encrypting and decrypting column values.
+ */
+
+#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,
+};
+
+#endif							/* TEST_ENCRYPT */
+
+
+/*
+ * Get OpenSSL cipher that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get OpenSSL digest (for MAC) that corresponds to the CEK algorithm number.
+ */
+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;
+}
+
+/*
+ * Get the MAC key length (in octets) that corresponds to the CEK algorithm number.
+ *
+ * This is MAC_KEY_LEN in the mcgrew paper.
+ */
+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;
+}
+
+/*
+ * Get the HMAC output length (in octets) that corresponds to the CEK
+ * algorithm number.
+ */
+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;
+}
+
+/*
+ * Length of associated data (A in mcgrew paper)
+ */
+#ifndef TEST_ENCRYPT
+#define PG_AD_LEN 4
+#else
+#define PG_AD_LEN sizeof(test_A)
+#endif
+
+/*
+ * Compute message authentication tag (T in the mcgrew paper), from MAC key
+ * and ciphertext.
+ *
+ * Returns false on error, with error message in errmsgp.
+ */
+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,
+					 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;
+	int64		al;
+	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;
+	}
+
+	/*
+	 * Build input to MAC call (A || S || AL in mcgrew paper)
+	 */
+	bufsize = PG_AD_LEN + encrlen + sizeof(int64);
+	buf = malloc(bufsize);
+	if (!buf)
+	{
+		*errmsgp = libpq_gettext("out of memory");
+		goto fail;
+	}
+	/* A (associated data) */
+#ifndef TEST_ENCRYPT
+	buf[0] = 'P';
+	buf[1] = 'G';
+	*(int16 *) (buf + 2) = pg_hton16(1);
+#else
+	memcpy(buf, test_A, sizeof(test_A));
+#endif
+	/* S (ciphertext) */
+	memcpy(buf + PG_AD_LEN, encr, encrlen);
+	/* AL (number of *bits* in A) */
+	al = pg_hton64(PG_AD_LEN * 8);
+	memcpy(buf + PG_AD_LEN + encrlen, &al, sizeof(al));
+
+	/*
+	 * Call MAC
+	 */
+	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;
+}
+
+/*
+ * Decrypt a column value
+ */
+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			iv_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 = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_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;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+
+	if (!get_message_auth_tag(md, mac_key, mac_key_len,
+							  input, inputlen - (md_hash_length(md) / 2),
+							  md_value, &md_len,
+							  errmsgp))
+	{
+		goto fail;
+	}
+
+	/* use constant-time comparison, per mcgrew paper */
+	if (CRYPTO_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;
+	buf = pqResultAlloc(res, bufsize, false);
+	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;
+}
+
+/*
+ * Compute a synthetic initialization vector (SIV), for deterministic
+ * encryption.
+ *
+ * Per protocol specification, the SIV is computed as:
+ *
+ * SUBSTRING(HMAC(K, P) FOR IVLEN)
+ */
+#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)
+	{
+		libpq_append_conn_error(conn, "could not allocate key for HMAC: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignInit(evp_md_ctx, NULL, md, NULL, pkey))
+	{
+		libpq_append_conn_error(conn, "digest initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignUpdate(evp_md_ctx, plaintext, plaintext_len))
+	{
+		libpq_append_conn_error(conn, "digest signing failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	if (!EVP_DigestSignFinal(evp_md_ctx, md_value, &md_len))
+	{
+		libpq_append_conn_error(conn, "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 */
+
+/*
+ * Encrypt a column value
+ */
+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			iv_key_len;
+	int			key_len;
+	const unsigned char *enc_key;
+	const unsigned char *mac_key;
+	const unsigned char *iv_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)
+	{
+		libpq_append_conn_error(conn, "unrecognized encryption algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	md = pg_cekalg_to_openssl_md(cekalg);
+	if (!md)
+	{
+		libpq_append_conn_error(conn, "unrecognized digest algorithm identifier: %d", cekalg);
+		goto fail;
+	}
+
+	evp_cipher_ctx = EVP_CIPHER_CTX_new();
+
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, cipher, NULL, NULL, NULL))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+
+	enc_key_len = EVP_CIPHER_CTX_key_length(evp_cipher_ctx);
+	mac_key_len = iv_key_len = md_key_length(md);
+	key_len = mac_key_len + enc_key_len + iv_key_len;
+
+	if (cek->cekdatalen != key_len)
+	{
+		libpq_append_conn_error(conn, "column encryption key has wrong length for algorithm (has: %zu, required: %d)",
+								cek->cekdatalen, key_len);
+		goto fail;
+	}
+
+	mac_key = cek->cekdata;
+	enc_key = cek->cekdata + mac_key_len;
+	iv_key = cek->cekdata + mac_key_len + enc_key_len;
+
+	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, iv_key, iv_key_len, value, nbytes);
+#else
+		(void) iv_key;			/* unused */
+		memcpy(iv, test_IV, ivlen);
+#endif
+	}
+	else
+		pg_strong_random(iv, ivlen);
+	if (!EVP_EncryptInit_ex(evp_cipher_ctx, NULL, NULL, enc_key, iv))
+	{
+		libpq_append_conn_error(conn, "encryption initialization failed: %s",
+								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)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		goto fail;
+	}
+	memcpy(buf, iv, ivlen);
+	encr = buf + ivlen;
+	if (!EVP_EncryptUpdate(evp_cipher_ctx, encr, &encrlen, value, nbytes))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								ERR_reason_error_string(ERR_get_error()));
+		goto fail;
+	}
+	if (!EVP_EncryptFinal_ex(evp_cipher_ctx, encr + encrlen, &encrlen2))
+	{
+		libpq_append_conn_error(conn, "encryption failed: %s",
+								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,
+							  md_value, &md_len,
+							  &errmsg))
+	{
+		appendPQExpBuffer(&conn->errorMessage, "%s\n", errmsg);
+		goto fail;
+	}
+
+	buf2size = encrlen + md_len;
+	buf2 = malloc(buf2size);
+	if (!buf2)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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");
+}
+
+/*
+ * K and P are from the mcgrew paper, K_len and P_len are their respective
+ * lengths.  encrypt_value() requires the key length to contain the IV key, so
+ * we pass it here, too, but it will not be used.
+ */
+static void
+test_case(int alg, const unsigned char *K, size_t K_len, size_t IV_key_len, const unsigned char *P, size_t P_len)
+{
+	unsigned char *C;
+	int			nbytes;
+	PGCEK		cek;
+
+	nbytes = P_len;
+	cek.cekdatalen = K_len + IV_key_len;
+	cek.cekdata = malloc(cek.cekdatalen);
+	memcpy(cek.cekdata, K, 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, 16, P, sizeof(P));
+	printf("5.2\n");
+	test_case(PG_CEK_AEAD_AES_192_CBC_HMAC_SHA_384, K, 48, 24, P, sizeof(P));
+	printf("5.3\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_384, K, 56, 24, P, sizeof(P));
+	printf("5.4\n");
+	test_case(PG_CEK_AEAD_AES_256_CBC_HMAC_SHA_512, K, 64, 32, 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 00000000000..252d271436c
--- /dev/null
+++ b/src/interfaces/libpq/fe-encrypt.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-encrypt.h
+ *
+ * client-side column encryption support
+ *
+ * Portions Copyright (c) 1996-2024, 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, const char *cmkfilename, 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 7bdfc4c21aa..fc1d5fe39d8 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -24,6 +24,8 @@
 #include <unistd.h>
 #endif
 
+#include "common/colenc.h"
+#include "fe-encrypt.h"
 #include "libpq-fe.h"
 #include "libpq-int.h"
 #include "mb/pg_wchar.h"
@@ -73,7 +75,8 @@ static int	PQsendQueryGuts(PGconn *conn,
 							const char *const *paramValues,
 							const int *paramLengths,
 							const int *paramFormats,
-							int resultFormat);
+							int resultFormat,
+							PGresult *paramDesc);
 static void parseInput(PGconn *conn);
 static PGresult *getCopyResult(PGconn *conn, ExecStatusType copytype);
 static bool PQexecStart(PGconn *conn);
@@ -1192,6 +1195,421 @@ pqSaveParameterStatus(PGconn *conn, const char *name, const char *value)
 	}
 }
 
+/*
+ * pqSaveColumnMasterKey - save column master key sent by backend
+ */
+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 *tmpfile)
+{
+	PQExpBufferData buf;
+
+	initPQExpBuffer(&buf);
+
+	for (const char *p = in; *p; p++)
+	{
+		if (p[0] == '%')
+		{
+			switch (p[1])
+			{
+				case 'a':
+					{
+						const char *s = get_cmkalg_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'j':
+					{
+						const char *s = get_cmkalg_jwa_name(cmkalg);
+
+						appendPQExpBufferStr(&buf, s ? s : "INVALID");
+					}
+					p++;
+					break;
+				case 'k':
+					appendPQExpBufferStr(&buf, cmkname);
+					p++;
+					break;
+				case 'p':
+					appendPQExpBufferStr(&buf, tmpfile);
+					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, const char *cmkfilename, int cmkalg,
+					  int fromlen, const unsigned char *from,
+					  int *tolen)
+{
+	libpq_append_conn_error(conn, "column encryption not supported by this build");
+	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;
+
+	if (!conn->cmklookup || !conn->cmklookup[0])
+	{
+		libpq_append_conn_error(conn, "column master key lookup is not configured");
+		return NULL;
+	}
+
+	cmklookup = strdup(conn->cmklookup ? conn->cmklookup : "");
+
+	if (!cmklookup)
+	{
+		libpq_append_conn_error(conn, "out of memory");
+		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)
+		{
+			libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", '=', 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)
+			{
+				libpq_append_conn_error(conn, "syntax error in CMK lookup specification, missing \"%c\": %s", ':', s);
+				goto fail;
+			}
+
+			if (strncmp(sep + 1, "file", sep2 - (sep + 1)) == 0)
+			{
+				char	   *cmkfilename;
+
+				cmkfilename = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, "INVALID");
+				result = decrypt_cek_from_file(conn, cmkfilename, cmkalg, fromlen, from, tolen);
+				free(cmkfilename);
+			}
+			else if (strncmp(sep + 1, "run", sep2 - (sep + 1)) == 0)
+			{
+				char		tmpfile[MAXPGPATH] = {0};
+				int			fd;
+				char	   *command;
+				FILE	   *fp;
+
+				/* only needs enough room for CEK key material */
+				char		buf[1024];
+				size_t		nread;
+				int			rc;
+
+#ifndef WIN32
+				{
+					const char *tmpdir;
+
+					tmpdir = getenv("TMPDIR");
+					if (!tmpdir)
+						tmpdir = "/tmp";
+					strlcpy(tmpfile, tmpdir, sizeof(tmpfile));
+					strlcat(tmpfile, "/libpq-XXXXXX", sizeof(tmpfile));
+					fd = mkstemp(tmpfile);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: %m");
+						goto fail;
+					}
+				}
+#else
+				{
+					char		tmpdir[MAXPGPATH];
+					int			ret;
+
+					ret = GetTempPath(MAXPGPATH, tmpdir);
+					if (ret == 0 || ret > MAXPGPATH)
+					{
+						libpq_append_conn_error(conn, "could not locate temporary directory: %s",
+												!ret ? strerror(errno) : "");
+						return false;
+					}
+
+					if (GetTempFileName(tmpdir, "libpq", 0, tmpfile) == 0)
+					{
+						libpq_append_conn_error(conn, "could not run create temporary file: error code %lu",
+												GetLastError());
+						goto fail;
+					}
+
+					fd = open(tmpfile, O_WRONLY | O_TRUNC | PG_BINARY, 0);
+					if (fd < 0)
+					{
+						libpq_append_conn_error(conn, "could not run open temporary file: %m");
+						goto fail;
+					}
+				}
+#endif
+				if (write(fd, from, fromlen) < fromlen)
+				{
+					libpq_append_conn_error(conn, "could not write to temporary file: %m");
+					close(fd);
+					unlink(tmpfile);
+					goto fail;
+				}
+				if (close(fd) < 0)
+				{
+					libpq_append_conn_error(conn, "could not close temporary file: %m");
+					unlink(tmpfile);
+					goto fail;
+				}
+
+				command = replace_cmk_placeholders(sep2 + 1, cmk->cmkname, cmk->cmkrealm, cmkalg, tmpfile);
+				fp = popen(command, PG_BINARY_R);
+				if (!fp)
+				{
+					libpq_append_conn_error(conn, "could not run command \"%s\": %m", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				nread = fread(buf, 1, sizeof(buf), fp);
+				if (ferror(fp))
+				{
+					libpq_append_conn_error(conn, "could not read from command: %m");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				else if (!feof(fp))
+				{
+					libpq_append_conn_error(conn, "output from command too long");
+					pclose(fp);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				rc = pclose(fp);
+				if (rc != 0)
+				{
+					/*
+					 * XXX would like to use wait_result_to_str(rc) but that
+					 * cannot be called from libpq because it calls exit()
+					 */
+					libpq_append_conn_error(conn, "could not run command \"%s\"", command);
+					free(command);
+					unlink(tmpfile);
+					goto fail;
+				}
+				free(command);
+				unlink(tmpfile);
+
+				result = malloc(nread);
+				if (!result)
+				{
+					libpq_append_conn_error(conn, "out of memory");
+					goto fail;
+				}
+				memcpy(result, buf, nread);
+				*tolen = nread;
+			}
+			else
+			{
+				libpq_append_conn_error(conn, "CMK lookup scheme \"%s\" not recognized", sep + 1);
+				goto fail;
+			}
+		}
+	}
+
+	if (!found)
+	{
+		libpq_append_conn_error(conn, "no CMK lookup found for realm \"%s\"", cmk->cmkrealm);
+	}
+
+fail:
+	free(cmklookup);
+	return result;
+}
+
+/*
+ * pqSaveColumnEncryptionKey - save column encryption key sent by backend
+ */
+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
@@ -1263,13 +1681,51 @@ pqRowProcessor(PGconn *conn, const char **errmsgp)
 			bool		isbinary = (res->attDescs[i].format != 0);
 			char	   *val;
 
-			val = (char *) pqResultAlloc(res, clen + 1, isbinary);
-			if (val == NULL)
+			if (res->attDescs[i].cekid)
+			{
+				/* encrypted column */
+#ifdef USE_SSL
+				PGCEK	   *cek = NULL;
+
+				if (!isbinary)
+				{
+					*errmsgp = libpq_gettext("encrypted column was not sent in binary format");
+					return 0;
+				}
+
+				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("protocol error: column encryption key associated with encrypted column was not sent by the server");
+					return 0;
+				}
+
+				val = (char *) decrypt_value(res, cek, res->attDescs[i].cekalg,
+											 (const unsigned char *) columns[i].value, clen, errmsgp);
+				if (val == NULL)
+					return 0;
+#else
+				*errmsgp = libpq_gettext("column encryption not supported by this build");
 				return 0;
+#endif
+			}
+			else
+			{
+				val = (char *) pqResultAlloc(res, clen + 1, isbinary);
+				if (val == NULL)
+					return 0;
 
-			/* copy and zero-terminate the data (even if it's binary) */
-			memcpy(val, columns[i].value, clen);
-			val[clen] = '\0';
+				/* 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;
@@ -1498,6 +1954,8 @@ PQsendQueryParams(PGconn *conn,
 				  const int *paramFormats,
 				  int resultFormat)
 {
+	PGresult   *paramDesc = NULL;
+
 	if (!PQsendQueryStart(conn, true))
 		return 0;
 
@@ -1514,6 +1972,37 @@ PQsendQueryParams(PGconn *conn,
 		return 0;
 	}
 
+	if (conn->column_encryption_enabled)
+	{
+		PGresult   *res;
+		bool		error;
+
+		if (conn->pipelineStatus != PQ_PIPELINE_OFF)
+		{
+			libpq_append_conn_error(conn, "synchronous command execution functions are not allowed in pipeline mode");
+			return 0;
+		}
+
+		if (!PQsendPrepare(conn, "", command, nParams, paramTypes))
+			return 0;
+		error = false;
+		while ((res = PQgetResult(conn)) != NULL)
+		{
+			if (PQresultStatus(res) != PGRES_COMMAND_OK)
+				error = true;
+			PQclear(res);
+		}
+		if (error)
+			return 0;
+
+		paramDesc = PQdescribePrepared(conn, "");
+		if (PQresultStatus(paramDesc) != PGRES_COMMAND_OK)
+			return 0;
+
+		command = NULL;
+		paramTypes = NULL;
+	}
+
 	return PQsendQueryGuts(conn,
 						   command,
 						   "",	/* use unnamed statement */
@@ -1522,7 +2011,8 @@ PQsendQueryParams(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1637,6 +2127,24 @@ PQsendQueryPrepared(PGconn *conn,
 					const int *paramLengths,
 					const int *paramFormats,
 					int resultFormat)
+{
+	return PQsendQueryPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQsendQueryPreparedDescribed
+ *		Like PQsendQueryPrepared, but with additional argument to pass
+ *		parameter descriptions, for column encryption.
+ */
+int
+PQsendQueryPreparedDescribed(PGconn *conn,
+							 const char *stmtName,
+							 int nParams,
+							 const char *const *paramValues,
+							 const int *paramLengths,
+							 const int *paramFormats,
+							 int resultFormat,
+							 PGresult *paramDesc)
 {
 	if (!PQsendQueryStart(conn, true))
 		return 0;
@@ -1662,7 +2170,8 @@ PQsendQueryPrepared(PGconn *conn,
 						   paramValues,
 						   paramLengths,
 						   paramFormats,
-						   resultFormat);
+						   resultFormat,
+						   paramDesc);
 }
 
 /*
@@ -1762,7 +2271,8 @@ PQsendQueryGuts(PGconn *conn,
 				const char *const *paramValues,
 				const int *paramLengths,
 				const int *paramFormats,
-				int resultFormat)
+				int resultFormat,
+				PGresult *paramDesc)
 {
 	int			i;
 	PGcmdQueueEntry *entry;
@@ -1810,13 +2320,47 @@ PQsendQueryGuts(PGconn *conn,
 		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;
+
+			/* Check force column encryption */
+			if (format & 0x10)
+			{
+				if (!(paramDesc &&
+					  paramDesc->paramDescs &&
+					  paramDesc->paramDescs[i].cekid))
+				{
+					libpq_append_conn_error(conn, "parameter with forced encryption is not to be encrypted");
+					goto sendFailed;
+				}
+			}
+			format &= ~0x10;
+
+			if (paramDesc && paramDesc->paramDescs)
+			{
+				PGresParamDesc *pd = &paramDesc->paramDescs[i];
+
+				if (pd->cekid)
+				{
+					if (format != 0)
+					{
+						libpq_append_conn_error(conn, "format must be text for encrypted parameter");
+						goto sendFailed;
+					}
+					/* Send encrypted value in binary */
+					format = 1;
+					/* And mark it as encrypted */
+					format |= 0x10;
+				}
+			}
+
+			if (pqPutInt(format, 2, conn) < 0)
 				goto sendFailed;
 		}
 	}
@@ -1835,8 +2379,9 @@ PQsendQueryGuts(PGconn *conn,
 		if (paramValues && paramValues[i])
 		{
 			int			nbytes;
+			const char *paramValue;
 
-			if (paramFormats && paramFormats[i] != 0)
+			if (paramFormats && (paramFormats[i] & 0x01) != 0)
 			{
 				/* binary parameter */
 				if (paramLengths)
@@ -1852,9 +2397,53 @@ PQsendQueryGuts(PGconn *conn,
 				/* text parameter, do not use paramLengths */
 				nbytes = strlen(paramValues[i]);
 			}
-			if (pqPutInt(nbytes, 4, conn) < 0 ||
-				pqPutnchar(paramValues[i], nbytes, conn) < 0)
+
+			paramValue = paramValues[i];
+
+			if (paramDesc && paramDesc->paramDescs && paramDesc->paramDescs[i].cekid)
+			{
+				/* encrypted column */
+#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)
+				{
+					libpq_append_conn_error(conn, "protocol error: column encryption key associated with encrypted parameter was not sent by the server");
+					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
+				libpq_append_conn_error(conn, "column encryption not supported by this build");
 				goto sendFailed;
+#endif
+			}
+			else
+			{
+				if (pqPutInt(nbytes, 4, conn) < 0 ||
+					pqPutnchar(paramValue, nbytes, conn) < 0)
+					goto sendFailed;
+			}
 		}
 		else
 		{
@@ -2327,12 +2916,31 @@ PQexecPrepared(PGconn *conn,
 			   const int *paramLengths,
 			   const int *paramFormats,
 			   int resultFormat)
+{
+	return PQexecPreparedDescribed(conn, stmtName, nParams, paramValues, paramLengths, paramFormats, resultFormat, NULL);
+}
+
+/*
+ * PQexecPreparedDescribed
+ *		Like PQexecPrepared, but with additional argument to pass parameter
+ *		descriptions, for column encryption.
+ */
+PGresult *
+PQexecPreparedDescribed(PGconn *conn,
+						const char *stmtName,
+						int nParams,
+						const char *const *paramValues,
+						const int *paramLengths,
+						const int *paramFormats,
+						int resultFormat,
+						PGresult *paramDesc)
 {
 	if (!PQexecStart(conn))
 		return NULL;
-	if (!PQsendQueryPrepared(conn, stmtName,
-							 nParams, paramValues, paramLengths,
-							 paramFormats, resultFormat))
+	if (!PQsendQueryPreparedDescribed(conn, stmtName,
+									  nParams, paramValues, paramLengths,
+									  paramFormats,
+									  resultFormat, paramDesc))
 		return NULL;
 	return PQexecFinish(conn);
 }
@@ -3710,7 +4318,17 @@ PQfformat(const PGresult *res, int field_num)
 	if (!check_field_number(res, field_num))
 		return 0;
 	if (res->attDescs)
+	{
+		/*
+		 * An encrypted column is always presented to the application in text
+		 * format.  The .format field applies to the ciphertext, which might
+		 * be in either format, but the plaintext inside is always in text
+		 * format.
+		 */
+		if (res->attDescs[field_num].cekid != 0)
+			return 0;
 		return res->attDescs[field_num].format;
+	}
 	else
 		return 0;
 }
@@ -3748,6 +4366,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)
 {
@@ -3933,6 +4562,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 3170d484f02..c453546b46d 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -48,6 +48,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);
@@ -313,6 +315,12 @@ pqParseInput3(PGconn *conn)
 					if (pqGetInt(&(conn->be_key), 4, conn))
 						return;
 					break;
+				case PqMsg_ColumnMasterKey:
+					getColumnMasterKey(conn);
+					break;
+				case PqMsg_ColumnEncryptionKey:
+					getColumnEncryptionKey(conn);
+					break;
 				case PqMsg_RowDescription:
 					if (conn->error_result ||
 						(conn->result != NULL &&
@@ -374,8 +382,21 @@ pqParseInput3(PGconn *conn)
 					}
 					break;
 				case PqMsg_ParameterDescription:
-					if (getParamDescriptions(conn, msgLength))
-						return;
+					if (conn->error_result ||
+						(conn->result != NULL &&
+						 conn->result->resultStatus == PGRES_FATAL_ERROR))
+					{
+						/*
+						 * We've already choked for some reason.  Just discard
+						 * the data till we get to the end of the query.
+						 */
+						conn->inCursor += msgLength;
+					}
+					else
+					{
+						if (getParamDescriptions(conn, msgLength))
+							return;
+					}
 					break;
 				case PqMsg_DataRow:
 					if (conn->result != NULL &&
@@ -564,6 +585,9 @@ getRowDescriptions(PGconn *conn, int msgLength)
 		int			typlen;
 		int			atttypmod;
 		int			format;
+		int			cekid;
+		int			cekalg;
+		int			flags;
 
 		if (pqGets(&conn->workBuffer, conn) ||
 			pqGetInt(&tableid, 4, conn) ||
@@ -578,6 +602,23 @@ getRowDescriptions(PGconn *conn, int msgLength)
 			goto advance_and_error;
 		}
 
+		if (conn->column_encryption_enabled)
+		{
+			if (pqGetInt(&cekid, 4, conn) ||
+				pqGetInt(&cekalg, 4, conn) ||
+				pqGetInt(&flags, 2, conn))
+			{
+				errmsg = libpq_gettext("insufficient data in \"T\" message");
+				goto advance_and_error;
+			}
+		}
+		else
+		{
+			cekid = 0;
+			cekalg = 0;
+			flags = 0;
+		}
+
 		/*
 		 * Since pqGetInt treats 2-byte integers as unsigned, we need to
 		 * coerce these results to signed form.
@@ -599,6 +640,8 @@ 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)
 			result->binary = 0;
@@ -702,10 +745,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, 4, 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! */
@@ -1486,6 +1550,92 @@ 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);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	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;
+	int			ret;
+
+	/* 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, 4, 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 */
+	ret = pqSaveColumnEncryptionKey(conn, keyid, cmkid, cmkalg, (unsigned char *) buf, vallen);
+	if (ret != 0)
+		pqSaveErrorResult(conn);
+
+	free(buf);
+
+	return ret;
+}
 
 /*
  * Attempt to read a Notify response message.
@@ -2304,6 +2454,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 c9932fc8a6b..740dd15e5b2 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);
+			pqTraceOutputInt32(f, message, cursor, false);
+			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,12 @@ 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);
+			pqTraceOutputInt32(f, message, cursor, false);
+			pqTraceOutputInt16(f, message, cursor);
+		}
 	}
 }
 
@@ -514,6 +530,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);
+	pqTraceOutputInt32(f, message, cursor, false);
+	len = pqTraceOutputInt32(f, message, cursor, false);
+	pqTraceOutputNchar(f, len, message, cursor);
+}
+
 /* ReadyForQuery */
 static void
 pqTraceOutputZ(FILE *f, const char *message, int *cursor)
@@ -657,10 +697,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 				fprintf(conn->Pfdebug, "Sync"); /* no message content */
 			break;
 		case PqMsg_ParameterDescription:
-			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputt(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case PqMsg_RowDescription:
-			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress);
+			pqTraceOutputT(conn->Pfdebug, message, &logCursor, regress,
+						   conn->column_encryption_enabled);
 			break;
 		case PqMsg_NegotiateProtocolVersion:
 			pqTraceOutputv(conn->Pfdebug, message, &logCursor);
@@ -675,6 +717,12 @@ pqTraceOutputMessage(PGconn *conn, const char *message, bool toServer)
 			fprintf(conn->Pfdebug, "Terminate");
 			/* No message content */
 			break;
+		case PqMsg_ColumnMasterKey:
+			pqTraceOutputy(conn->Pfdebug, message, &logCursor, regress);
+			break;
+		case PqMsg_ColumnEncryptionKey:
+			pqTraceOutputY(conn->Pfdebug, message, &logCursor, regress);
+			break;
 		case PqMsg_ReadyForQuery:
 			pqTraceOutputZ(conn->Pfdebug, message, &logCursor);
 			break;
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index 73f6e65ae55..91717ab23b9 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -276,6 +276,8 @@ typedef struct pgresAttDesc
 	Oid			typid;			/* type id */
 	int			typlen;			/* type size */
 	int			atttypmod;		/* type-specific modifier info */
+	Oid			cekid;
+	int			cekalg;
 } PGresAttDesc;
 
 /* ----------------
@@ -466,6 +468,14 @@ extern PGresult *PQexecPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern PGresult *PQexecPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 
 /* Interface for multiple-result or asynchronous queries */
 #define PQ_QUERY_PARAM_MAX_LIMIT 65535
@@ -489,6 +499,14 @@ extern int	PQsendQueryPrepared(PGconn *conn,
 								const int *paramLengths,
 								const int *paramFormats,
 								int resultFormat);
+extern int	PQsendQueryPreparedDescribed(PGconn *conn,
+										 const char *stmtName,
+										 int nParams,
+										 const char *const *paramValues,
+										 const int *paramLengths,
+										 const int *paramFormats,
+										 int resultFormat,
+										 PGresult *paramDesc);
 extern int	PQsetSingleRowMode(PGconn *conn);
 extern int	PQsetChunkedRowsMode(PGconn *conn, int chunkSize);
 extern PGresult *PQgetResult(PGconn *conn);
@@ -561,6 +579,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 */
@@ -570,6 +589,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 3691e5ee969..7b1c7133379 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -113,6 +113,9 @@ union pgresult_data
 typedef struct pgresParamDesc
 {
 	Oid			typid;			/* type id */
+	Oid			cekid;
+	int			cekalg;
+	int			flags;
 } PGresParamDesc;
 
 /*
@@ -358,6 +361,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.
@@ -416,6 +439,9 @@ struct pg_conn
 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
 	char	   *target_session_attrs;	/* desired session properties */
 	char	   *require_auth;	/* name of the expected auth method */
+	char	   *cmklookup;		/* CMK lookup specification */
+	char	   *column_encryption_setting;	/* column_encryption connection
+											 * parameter */
 	char	   *load_balance_hosts; /* load balance over hosts */
 
 	bool		cancelRequest;	/* true if this connection is used to send a
@@ -517,8 +543,15 @@ 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 */
 	pg_prng_state prng_state;	/* prng state for load balancing connections */
 
+	/* 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 */
@@ -709,6 +742,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, bool isReadyForQuery,
 								  bool gotSync);
diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build
index be6fadaea23..60fbdce602f 100644
--- a/src/interfaces/libpq/meson.build
+++ b/src/interfaces/libpq/meson.build
@@ -30,6 +30,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
 
@@ -118,6 +119,7 @@ tests += {
       't/002_api.pl',
       't/003_load_balance_host_list.pl',
       't/004_load_balance_dns.pl',
+      't/010_encrypt.pl',
     ],
     'env': {'with_ssl': ssl_library},
   },
diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk
index 40b662dc08b..b6e9743a98a 100644
--- a/src/interfaces/libpq/nls.mk
+++ b/src/interfaces/libpq/nls.mk
@@ -3,6 +3,7 @@ CATALOG_NAME     = libpq
 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 \
diff --git a/src/interfaces/libpq/t/010_encrypt.pl b/src/interfaces/libpq/t/010_encrypt.pl
new file mode 100644
index 00000000000..db588a52563
--- /dev/null
+++ b/src/interfaces/libpq/t/010_encrypt.pl
@@ -0,0 +1,72 @@
+# Copyright (c) 2024, PostgreSQL Global Development Group
+use strict;
+use warnings;
+
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'OpenSSL not supported by this build'
+  if $ENV{with_ssl} ne 'openssl';
+
+# test data from https://datatracker.ietf.org/doc/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5
+command_like(
+	['libpq_test_encrypt'],
+	qr{5.1
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+c8 0e df a3 2d df 39 d5 ef 00 c0 b4 68 83 42 79
+a2 e4 6a 1b 80 49 f7 92 f7 6b fe 54 b9 03 a9 c9
+a9 4a c9 b4 7a d2 65 5c 5f 10 f9 ae f7 14 27 e2
+fc 6f 9b 3f 39 9a 22 14 89 f1 63 62 c7 03 23 36
+09 d4 5a c6 98 64 e3 32 1c f8 29 35 ac 40 96 c8
+6e 13 33 14 c5 40 19 e8 ca 79 80 df a4 b9 cf 1b
+38 4c 48 6f 3a 54 c5 10 78 15 8e e5 d7 9d e5 9f
+bd 34 d8 48 b3 d6 95 50 a6 76 46 34 44 27 ad e5
+4b 88 51 ff b5 98 f7 f8 00 74 b9 47 3c 82 e2 db
+65 2c 3f a3 6b 0a 7c 5b 32 19 fa b3 a3 0b c1 c4
+5.2
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+ea 65 da 6b 59 e6 1e db 41 9b e6 2d 19 71 2a e5
+d3 03 ee b5 00 52 d0 df d6 69 7f 77 22 4c 8e db
+00 0d 27 9b dc 14 c1 07 26 54 bd 30 94 42 30 c6
+57 be d4 ca 0c 9f 4a 84 66 f2 2b 22 6d 17 46 21
+4b f8 cf c2 40 0a dd 9f 51 26 e4 79 66 3f c9 0b
+3b ed 78 7a 2f 0f fc bf 39 04 be 2a 64 1d 5c 21
+05 bf e5 91 ba e2 3b 1d 74 49 e5 32 ee f6 0a 9a
+c8 bb 6c 6b 01 d3 5d 49 78 7b cd 57 ef 48 49 27
+f2 80 ad c9 1a c0 c4 e7 9c 7b 11 ef c6 00 54 e3
+84 90 ac 0e 58 94 9b fe 51 87 5d 73 3f 93 ac 20
+75 16 80 39 cc c7 33 d7
+5.3
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+89 31 29 b0 f4 ee 9e b1 8d 75 ed a6 f2 aa a9 f3
+60 7c 98 c4 ba 04 44 d3 41 62 17 0d 89 61 88 4e
+58 f2 7d 4a 35 a5 e3 e3 23 4a a9 94 04 f3 27 f5
+c2 d7 8e 98 6e 57 49 85 8b 88 bc dd c2 ba 05 21
+8f 19 51 12 d6 ad 48 fa 3b 1e 89 aa 7f 20 d5 96
+68 2f 10 b3 64 8d 3b b0 c9 83 c3 18 5f 59 e3 6d
+28 f6 47 c1 c1 39 88 de 8e a0 d8 21 19 8c 15 09
+77 e2 8c a7 68 08 0b c7 8c 35 fa ed 69 d8 c0 b7
+d9 f5 06 23 21 98 a4 89 a1 a6 ae 03 a3 19 fb 30
+dd 13 1d 05 ab 34 67 dd 05 6f 8e 88 2b ad 70 63
+7f 1e 9a 54 1d 9c 23 e7
+5.4
+C =
+1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04
+4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd
+3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd
+82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2
+e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b
+36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1
+1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3
+a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e
+31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b
+be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6
+4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf
+2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5
+},
+	'AEAD_AES_*_CBC_HMAC_SHA_* test cases');
+
+done_testing();
diff --git a/src/interfaces/libpq/test/.gitignore b/src/interfaces/libpq/test/.gitignore
index 6ba78adb678..1846594ec51 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 4e17ec15141..11b66337b83 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:
 	rm -f $(PROGS) *.o
diff --git a/src/interfaces/libpq/test/meson.build b/src/interfaces/libpq/test/meson.build
index 21dd37f69bc..666771a99e8 100644
--- a/src/interfaces/libpq/test/meson.build
+++ b/src/interfaces/libpq/test/meson.build
@@ -36,3 +36,26 @@ testprep_targets += executable('libpq_testclient',
     'install': false,
   }
 )
+
+
+libpq_test_encrypt_sources = files(
+  '../fe-encrypt-openssl.c',
+)
+
+if host_system == 'windows'
+  libpq_test_encrypt_sources += rc_bin_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'libpq_test_encrypt',
+    '--FILEDESC', 'libpq test program',])
+endif
+
+if ssl.found()
+  executable('libpq_test_encrypt',
+    libpq_test_encrypt_sources,
+    include_directories: include_directories('../../../port'),
+    dependencies: [frontend_code, libpq, ssl],
+    c_args: ['-DTEST_ENCRYPT'],
+    kwargs: default_bin_args + {
+      'install': false,
+    }
+  )
+endif
diff --git a/src/test/Makefile b/src/test/Makefile
index dbd3192874d..c9a38680531 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),column_encryption examples kerberos icu ldap ssl)
 
 # 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 00000000000..456dbf69d2a
--- /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 00000000000..7b6471e43e0
--- /dev/null
+++ b/src/test/column_encryption/Makefile
@@ -0,0 +1,31 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/column_encryption
+#
+# Portions Copyright (c) 1996-2024, 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
+
+export OPENSSL
+
+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 00000000000..84cfa84e12f
--- /dev/null
+++ b/src/test/column_encryption/meson.build
@@ -0,0 +1,23 @@
+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',
+    ],
+    'env': {
+      'OPENSSL': openssl.path(),
+    },
+  },
+}
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 00000000000..7a4c86e51f6
--- /dev/null
+++ b/src/test/column_encryption/t/001_column_encryption.pl
@@ -0,0 +1,307 @@
+# Copyright (c) 2021-2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $openssl = $ENV{OPENSSL};
+
+my $perlbin = $^X;
+$perlbin =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+
+# Can be changed manually for testing other algorithms.  Note that
+# RSAES_OAEP_SHA_256 requires OpenSSL 1.1.0.
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+sub create_cek
+{
+	my ($cekname, $bytes, $cmkname, $cmkfilename) = @_;
+
+	my $digest = $cmkalg;
+	$digest =~ s/.*(?=SHA)//;
+	$digest =~ s/_//g;
+
+	# generate random bytes
+	system_or_bail $openssl, 'rand', '-out',
+	  "${PostgreSQL::Test::Utils::tmp_check}/${cekname}.bin", $bytes;
+
+	# encrypt CEK using CMK
+	my @cmd = (
+		$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");
+	if ($digest ne 'SHA1')
+	{
+		# These options require OpenSSL >=1.1.0, so if the digest is
+		# SHA1, which is the default, omit the options.
+		push @cmd,
+		  '-pkeyopt', "rsa_mgf1_md:$digest",
+		  '-pkeyopt', "rsa_oaep_md:$digest";
+	}
+	system_or_bail @cmd;
+
+	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}, algorithm = '${cmkalg}', encrypted_value = '\\x${cekenchex}');}
+	);
+
+	return;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+my $cmk2filename = create_cmk('cmk2');
+create_cek('cek1', 48, 'cmk1', $cmk1filename);
+create_cek('cek2', 72, 'cmk2', $cmk2filename);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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', q{
+INSERT INTO tbl1 (a, b, c) VALUES (1, $1, $2) \bind 'val1' 11 \g
+INSERT INTO tbl1 (a, b, c) VALUES (2, $1, $2) \bind 'val2' 22 \g
+});
+
+# 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\tencrypted\$[0-9a-f]{96}\tencrypted\$[0-9a-f]{96}\n2\tencrypted\$[0-9a-f]{96}\tencrypted\$[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 %p';
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1});
+	isnt($result, 0, 'query fails with broken cmklookup run setting');
+}
+
+{
+	local $ENV{TESTWORKDIR} = ${PostgreSQL::Test::Utils::tmp_check};
+	local $ENV{PGCMKLOOKUP} =
+	  qq{*=run:"$perlbin" ./test_run_decrypt.pl %k %a %p};
+
+	my $stdout;
+	$result = $node->psql('postgres', q{SELECT a, b, c FROM tbl1},
+		stdout => \$stdout);
+	is( $stdout,
+		q(1|val1|11
+2|val2|22),
+		'decrypted query result with cmklookup run');
+}
+
+
+$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\tencrypted\$[0-9a-f]{96}/,
+	'inserted data is encrypted');
+
+
+# Test copy and restore
+
+my $copy_out = $node->safe_psql('postgres', q{COPY tbl1 TO STDOUT;});
+$node->safe_psql('postgres',
+	q{CREATE TABLE tbl1_copy (LIKE tbl1 INCLUDING ENCRYPTED)});
+$node->safe_psql('postgres',
+	q{COPY tbl1_copy FROM STDIN;} . "\n" . $copy_out . "\\\.\n");
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl1_copy});
+is( $result,
+	q(1|val1|11
+2|val2|22
+3|val3|33),
+	'decrypted query result after COPY dump and restore');
+
+
+# Tests with binary format
+
+# Supplying a parameter in binary format when the parameter is to be
+# encrypted results in an error from libpq.
+$node->command_fails_like(
+	[ 'test_client', 'test3' ],
+	qr/format must be text for encrypted parameter/,
+	'test client fails because to-be-encrypted parameter is in binary format'
+);
+
+# Requesting a binary result set still causes any encrypted columns to
+# be returned as text from the libpq API.
+$node->command_like(
+	[ 'test_client', 'test4' ],
+	qr/<0,0>=1:\n<0,1>=0:val1\n<0,2>=0:11/,
+	'binary result set with encrypted columns: encrypted columns returned as text'
+);
+
+
+# Test UPDATE
+
+$node->safe_psql(
+	'postgres', q{
+UPDATE tbl1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd' \g
+});
+
+$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 update');
+
+
+# Test views
+
+$node->safe_psql('postgres', q{CREATE VIEW v1 AS SELECT a, b, c FROM tbl1});
+
+$node->safe_psql('postgres',
+	q{UPDATE v1 SET b = $2 WHERE a = $1 \bind '3' 'val3upd2' \g});
+
+$result =
+  $node->safe_psql('postgres', q{SELECT a, b, c FROM v1 WHERE a IN (1, 3)});
+is( $result,
+	q(1|val1|11
+3|val3upd2|33),
+	'decrypted query result from view');
+
+
+# Test deterministic encryption
+
+$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', q{
+INSERT INTO tbl2 (a, b) VALUES ($1, $2), ($3, $4), ($5, $6) \bind '1' 'valA' '2' 'valB' '3' 'valA' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b FROM tbl2});
+is( $result,
+	q(1|valA
+2|valB
+3|valA),
+	'decrypted query result in table for deterministic encryption');
+
+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');
+
+is( $node->safe_psql(
+		'postgres', q{SELECT a FROM tbl2 WHERE b = $1 \bind 'valB' \g}),
+	q(2),
+	'select by deterministically encrypted column');
+
+
+# Test multiple keys in one table
+
+$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 tbl3 (a, b, c) VALUES (1, $1, $2) \bind 'valB1' 'valC1' \g
+});
+
+$result = $node->safe_psql('postgres', q{SELECT a, b, c FROM tbl3});
+is($result, q(1|valB1|valC1), 'decrypted query result multiple keys');
+
+$node->safe_psql(
+	'postgres', q{
+INSERT INTO tbl3 (a, b, c) VALUES ($1, $2, $3), ($4, $5, $6) \bind '2' 'valB2' 'valC2' '3' 'valB3' 'valC3' \g
+});
+
+$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 second insert');
+
+
+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 00000000000..2f2dac4e143
--- /dev/null
+++ b/src/test/column_encryption/t/002_cmk_rotation.pl
@@ -0,0 +1,125 @@
+# Copyright (c) 2021-2024, 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 $openssl = $ENV{OPENSSL};
+
+my $cmkalg = 'RSAES_OAEP_SHA_1';
+
+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}});
+	return $cmkfilename;
+}
+
+
+my $cmk1filename = create_cmk('cmk1');
+
+# create CEK
+my ($cekname, $bytes) = ('cek1', 48);
+
+# 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, algorithm = '$cmkalg', encrypted_value = '\\x${cekenchex}');}
+);
+
+$ENV{PGCOLUMNENCRYPTION} = 'on';
+$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) \bind 'val1' \g
+INSERT INTO tbl1 (a, b) VALUES (2, $1) \bind 'val2' \g
+});
+
+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, algorithm = '$cmkalg', 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 00000000000..c5df88d47d2
--- /dev/null
+++ b/src/test/column_encryption/test_client.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021-2024, PostgreSQL Global Development Group
+ */
+
+#include "postgres_fe.h"
+
+#include "libpq-fe.h"
+
+
+/*
+ * Test calls that don't support encryption
+ */
+static int
+test1(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {"3", "val3", "33"};
+
+	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;
+	}
+
+	res = PQexecPrepared(conn, "", 3, values, NULL, NULL, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test forced encryption
+ */
+static int
+test2(PGconn *conn)
+{
+	PGresult   *res,
+			   *res2;
+	const char *values[] = {"3", "val3", "33"};
+	int			formats[] = {0x00, 0x10, 0x00};
+
+	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 = PQexecPreparedDescribed(conn, "", 3, values, NULL, formats, 0, res2);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecPrepared() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you supply a binary parameter that is required to be
+ * encrypted.
+ */
+static int
+test3(PGconn *conn)
+{
+	PGresult   *res;
+	const char *values[] = {""};
+	int			lengths[] = {1};
+	int			formats[] = {1};
+
+	res = PQexecParams(conn, "INSERT INTO tbl1 (a, b, c) VALUES (100, NULL, $1)",
+					   3, NULL, values, lengths, formats, 0);
+	if (PQresultStatus(res) != PGRES_COMMAND_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	return 0;
+}
+
+/*
+ * Test what happens when you request results in binary and the result rows
+ * contain an encrypted column.
+ */
+static int
+test4(PGconn *conn)
+{
+	PGresult   *res;
+
+	res = PQexecParams(conn, "SELECT a, b, c FROM tbl1 WHERE a = 1", 0, NULL, NULL, NULL, NULL, 1);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+	{
+		fprintf(stderr, "PQexecParams() failed: %s\n",
+				PQerrorMessage(conn));
+		return 1;
+	}
+
+	for (int row = 0; row < PQntuples(res); row++)
+		for (int col = 0; col < PQnfields(res); col++)
+			printf("<%d,%d>=%d:%s\n", row, col, PQfformat(res, col), PQgetvalue(res, row, col));
+	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
+		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 00000000000..fef25dc6785
--- /dev/null
+++ b/src/test/column_encryption/test_run_decrypt.pl
@@ -0,0 +1,61 @@
+#!/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-2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+
+my ($cmkname, $alg, $filename) = @ARGV;
+
+die unless $alg =~ 'RSAES_OAEP_SHA';
+
+my $digest = $alg;
+$digest =~ s/.*(?=SHA)//;
+$digest =~ s/_//g;
+
+my $tmpdir = $ENV{TESTWORKDIR};
+
+my $openssl = $ENV{OPENSSL};
+
+my @cmd = (
+	$openssl, 'pkeyutl',
+	'-decrypt', '-inkey',
+	"${tmpdir}/${cmkname}.pem", '-pkeyopt',
+	'rsa_padding_mode:oaep', '-in',
+	$filename, '-out',
+	"${tmpdir}/output.tmp");
+
+if ($digest ne 'SHA1')
+{
+	# These options require OpenSSL >=1.1.0, so if the digest is
+	# SHA1, which is the default, omit the options.
+	push @cmd,
+	  '-pkeyopt', "rsa_mgf1_md:$digest",
+	  '-pkeyopt', "rsa_oaep_md:$digest";
+}
+
+system(@cmd) == 0 or die "system failed: $?";
+
+open my $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}/output.tmp";
+
+binmode STDOUT;
+
+print $data;
diff --git a/src/test/meson.build b/src/test/meson.build
index 702213bc6f6..213f7edf66e 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -10,6 +10,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 00000000000..b64b3241b93
--- /dev/null
+++ b/src/test/regress/expected/column_encryption.out
@@ -0,0 +1,541 @@
+\set HIDE_COLUMN_ENCRYPTION false
+CREATE ROLE regress_enc_user1;
+CREATE COLUMN MASTER KEY fail WITH (
+    foo = bar
+);
+ERROR:  column master key attribute "foo" not recognized
+LINE 2:     foo = bar
+            ^
+CREATE COLUMN MASTER KEY fail WITH (
+    realm = 'test',
+    realm = 'test'
+);
+ERROR:  conflicting or redundant options
+LINE 3:     realm = 'test'
+            ^
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+-- duplicate
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+ERROR:  column master key "cmk1" already exists
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+CREATE COLUMN MASTER KEY cmk2;
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+-- fail
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2', realm = 'test2');
+ERROR:  conflicting or redundant options
+LINE 1: ALTER COLUMN MASTER KEY cmk2a (realm = 'test2', realm = 'tes...
+                                                        ^
+-- fail
+ALTER COLUMN MASTER KEY cmk2a (foo = bar);
+ERROR:  column master key attribute "foo" not recognized
+LINE 1: ALTER COLUMN MASTER KEY cmk2a (foo = bar);
+                                       ^
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    foo = bar
+);
+ERROR:  column encryption key attribute "foo" not recognized
+LINE 2:     foo = bar
+            ^
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    column_master_key = cmk1
+);
+ERROR:  conflicting or redundant options
+LINE 3:     column_master_key = cmk1
+            ^
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  attribute "column_master_key" must be specified
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  attribute "algorithm" must be specified
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1'
+);
+ERROR:  attribute "encrypted_value" must be specified
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  unrecognized encryption algorithm: foo
+LINE 3:     algorithm = 'foo',  -- invalid
+            ^
+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';
+-- duplicate
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  column encryption key "cek1" already exists
+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 (foo = bar)
+);
+ERROR:  unrecognized column encryption parameter: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (encryption_type = randomized)
+);
+ERROR:  column encryption key must be specified
+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_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+ERROR:  unrecognized encryption algorithm: foo
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+ERROR:  unrecognized encryption type: wrong
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, column_encryption_key = cek1)
+);
+ERROR:  conflicting or redundant options
+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, encryption_type = deterministic);
+\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       |              | 
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+\d+ tbl_4897
+                                        Table "public.tbl_4897"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | extended |            |              | 
+
+\d+ tbl_6978
+                                        Table "public.tbl_6978"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+\d+ view_3bc9
+                                 View "public.view_3bc9"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Description 
+--------+---------+-----------+----------+---------+----------+------------+-------------
+ a      | integer |           |          |         | plain    |            | 
+ b      | text    |           |          |         | extended |            | 
+ c      | text    |           |          |         | external | cek1       | 
+View definition:
+ SELECT a,
+    b,
+    c
+   FROM tbl_29f3;
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+\d+ tbl_2386
+                                        Table "public.tbl_2386"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | extended |            |              | 
+ c      | text    |           |          |         | external | cek1       |              | 
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+ERROR:  encrypted columns not yet implemented for this command
+\d+ tbl_2941
+-- test partition declarations
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+\d+ tbl_13fa
+                                  Partitioned table "public.tbl_13fa"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition key: RANGE (a)
+Partitions: tbl_13fa_1 FOR VALUES FROM (1) TO (100)
+
+\d+ tbl_13fa_1
+                                       Table "public.tbl_13fa_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+Partition of: tbl_13fa FOR VALUES FROM (1) TO (100)
+Partition constraint: ((a IS NOT NULL) AND (a >= 1) AND (a < 100))
+
+-- test inheritance
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  multiple inheritance of encrypted columns is not implemented
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+ERROR:  column "b" has an encryption specification conflict
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+\d+ tbl_36f3_a_1
+                                      Table "public.tbl_36f3_a_1"
+ Column |  Type   | Collation | Nullable | Default | Storage  | Encryption | Stats target | Description 
+--------+---------+-----------+----------+---------+----------+------------+--------------+-------------
+ a      | integer |           |          |         | plain    |            |              | 
+ b      | text    |           |          |         | external | cek1       |              | 
+ c      | integer |           |          |         | plain    |            |              | 
+Inherits: tbl_36f3_a
+
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+NOTICE:  moving and merging column "b" with inherited definition
+DETAIL:  User-specified column moved to the position of the inherited column.
+ERROR:  column "b" has an encryption specification conflict
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+ERROR:  permission denied for column master key cmk1
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+ERROR:  permission denied for column encryption key cek1
+CREATE TABLE tbl_7040 (a int);
+-- fail
+ALTER TABLE tbl_7040 ADD COLUMN b text ENCRYPTED WITH (column_encryption_key = cek1);
+ERROR:  permission denied for column encryption key cek1
+DROP TABLE tbl_7040;
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+ has_column_encryption_key_privilege 
+-------------------------------------
+ t
+(1 row)
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+ has_column_master_key_privilege 
+---------------------------------
+ t
+(1 row)
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+DROP COLUMN MASTER KEY cmk1 RESTRICT;  -- fail
+ERROR:  cannot drop column master key cmk1 because other objects depend on it
+DETAIL:  column encryption key data of column encryption key cek1 for column master key cmk1 depends on column master key cmk1
+column encryption key data of column encryption key cek4 for column 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 in schema "public"
+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 in schema "public"
+ALTER COLUMN ENCRYPTION KEY cekx RENAME TO ceky;  -- fail
+ERROR:  column encryption key "cekx" does not exist
+SET SESSION AUTHORIZATION '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 SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+         List of column encryption keys
+ Schema | Name |       Owner       | Master key 
+--------+------+-------------------+------------
+ public | cek3 | regress_enc_user1 | cmk2a
+ public | cek3 | regress_enc_user1 | cmk3
+(2 rows)
+
+\dcmk cmk3
+        List of column master keys
+ Schema | Name |       Owner       | Realm 
+--------+------+-------------------+-------
+ public | cmk3 | regress_enc_user1 | 
+(1 row)
+
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+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
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, encrypted_value = '\xDEADBEEF');  -- fail
+ERROR:  attribute "encrypted_value" must not be specified
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+ERROR:  column encryption key "fail" does not exist
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+NOTICE:  column encryption key "nonexistent" does not exist, skipping
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+ERROR:  column master key "fail" does not exist
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+NOTICE:  column master key "nonexistent" does not exist, skipping
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/expected/object_address.out b/src/test/regress/expected/object_address.out
index fc42d418bf1..8dbb4a847ac 100644
--- a/src/test/regress/expected/object_address.out
+++ b/src/test/regress/expected/object_address.out
@@ -38,6 +38,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -101,6 +103,7 @@ BEGIN
         ('materialized view'), ('foreign table'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
+        ('column encryption key'), ('column encryption key data'), ('column master key'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
         ('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
         ('text search parser'), ('text search dictionary'),
@@ -201,6 +204,24 @@ WARNING:  error for cast,{addr_nsp,zwei},{}: name list length must be exactly 1
 WARNING:  error for cast,{addr_nsp,zwei},{integer}: name list length must be exactly 1
 WARNING:  error for cast,{eins,zwei,drei},{}: name list length must be exactly 1
 WARNING:  error for cast,{eins,zwei,drei},{integer}: name list length must be exactly 1
+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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins},{}: column encryption key "eins" does not exist
+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},{}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{addr_nsp,zwei},{integer}: column encryption key "addr_nsp.zwei" does not exist
+WARNING:  error for column encryption key data,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column encryption key data,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
+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},{}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{addr_nsp,zwei},{integer}: column master key "addr_nsp.zwei" does not exist
+WARNING:  error for column master key,{eins,zwei,drei},{}: cross-database references are not implemented: eins.zwei.drei
+WARNING:  error for column master key,{eins,zwei,drei},{integer}: cross-database references are not implemented: eins.zwei.drei
 WARNING:  error for table constraint,{eins},{}: must specify relation and object name
 WARNING:  error for table constraint,{eins},{integer}: must specify relation and object name
 WARNING:  error for table constraint,{addr_nsp,zwei},{}: relation "addr_nsp" does not exist
@@ -409,6 +430,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}', '{}'),
@@ -505,6 +529,9 @@ subscription|NULL|regress_addr_sub|regress_addr_sub|t
 publication|NULL|addr_pub|addr_pub|t
 publication relation|NULL|NULL|addr_nsp.gentable in publication addr_pub|t
 publication namespace|NULL|NULL|addr_nsp in publication addr_pub_schema|t
+column master key|addr_nsp|addr_cmk|addr_nsp.addr_cmk|t
+column encryption key|addr_nsp|addr_cek|addr_nsp.addr_cek|t
+column encryption key data|NULL|NULL|of addr_nsp.addr_cek for addr_nsp.addr_cmk|t
 ---
 --- Cleanup resources
 ---
@@ -517,6 +544,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
@@ -547,6 +576,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
@@ -634,5 +666,8 @@ ORDER BY objects.classid, objects.objid, objects.objsubid;
 ("(""publication relation"",,,)")|("(""publication relation"",,)")|NULL
 ("(""publication namespace"",,,)")|("(""publication namespace"",,)")|NULL
 ("(""parameter ACL"",,,)")|("(""parameter ACL"",,)")|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 215eb899be3..63d3081e67e 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -74,6 +74,8 @@ 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 {attcollation} => pg_collation {oid}
+NOTICE:  checking pg_attribute {attcek} => pg_colenckey {oid}
+NOTICE:  checking pg_attribute {attusertypid} => pg_type {oid}
 NOTICE:  checking pg_class {relnamespace} => pg_namespace {oid}
 NOTICE:  checking pg_class {reltype} => pg_type {oid}
 NOTICE:  checking pg_class {reloftype} => pg_type {oid}
@@ -266,3 +268,9 @@ 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 {cmknamespace} => pg_namespace {oid}
+NOTICE:  checking pg_colmasterkey {cmkowner} => pg_authid {oid}
+NOTICE:  checking pg_colenckey {ceknamespace} => pg_namespace {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 9d047b21b88..996628b7aae 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -159,7 +159,8 @@ ORDER BY 1, 2;
  text                        | character varying
  timestamp without time zone | timestamp with time zone
  txid_snapshot               | pg_snapshot
-(4 rows)
+ pg_encrypted_det            | pg_encrypted_rnd
+(5 rows)
 
 SELECT DISTINCT p1.proargtypes[0]::regtype, p2.proargtypes[0]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -175,13 +176,15 @@ 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)
+ pg_encrypted_det            | pg_encrypted_rnd
+(8 rows)
 
 SELECT DISTINCT p1.proargtypes[1]::regtype, p2.proargtypes[1]::regtype
 FROM pg_proc AS p1, pg_proc AS p2
@@ -197,12 +200,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
@@ -872,6 +876,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)
 uuid_extract_timestamp(uuid)
 uuid_extract_version(uuid)
 -- restore normal output mode
diff --git a/src/test/regress/expected/type_sanity.out b/src/test/regress/expected/type_sanity.out
index 88d8f6c32d6..faa5d79e42d 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,
@@ -718,6 +720,8 @@ SELECT oid, typname, typtype, typelem, typarray
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 675c5676171..eb6a0c6ac48 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,7 @@ test: publication subscription
 # Another group of parallel tests
 # select_views depends on create_view
 # ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass column_encryption
 
 # ----------
 # Another group of parallel tests (JSON related)
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index 8aeed97be1a..a3dba8109b4 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -76,7 +76,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);
 
diff --git a/src/test/regress/sql/column_encryption.sql b/src/test/regress/sql/column_encryption.sql
new file mode 100644
index 00000000000..0c5f2af2da5
--- /dev/null
+++ b/src/test/regress/sql/column_encryption.sql
@@ -0,0 +1,371 @@
+\set HIDE_COLUMN_ENCRYPTION false
+
+CREATE ROLE regress_enc_user1;
+
+CREATE COLUMN MASTER KEY fail WITH (
+    foo = bar
+);
+
+CREATE COLUMN MASTER KEY fail WITH (
+    realm = 'test',
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+COMMENT ON COLUMN MASTER KEY cmk1 IS 'column master key';
+
+-- duplicate
+CREATE COLUMN MASTER KEY cmk1 WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk1a WITH (
+    realm = 'test'
+);
+
+CREATE COLUMN MASTER KEY cmk2;
+
+CREATE COLUMN MASTER KEY cmk2a WITH (
+    realm = 'testx'
+);
+
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2');
+
+-- fail
+ALTER COLUMN MASTER KEY cmk2a (realm = 'test2', realm = 'test2');
+
+-- fail
+ALTER COLUMN MASTER KEY cmk2a (foo = bar);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    foo = bar
+);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    column_master_key = cmk1
+);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    encrypted_value = '\xDEADBEEF'
+);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1'
+);
+
+CREATE COLUMN ENCRYPTION KEY fail WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'foo',  -- invalid
+    encrypted_value = '\xDEADBEEF'
+);
+
+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';
+
+-- duplicate
+CREATE COLUMN ENCRYPTION KEY cek1 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+
+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 (foo = bar)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (encryption_type = randomized)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = notexist)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, algorithm = 'foo')
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = wrong)
+);
+
+CREATE TABLE tbl_fail (
+    a int,
+    b text,
+    c text ENCRYPTED WITH (column_encryption_key = cek1, column_encryption_key = cek1)
+);
+
+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, encryption_type = deterministic);
+
+\d tbl_447f
+\d+ tbl_447f
+
+CREATE TABLE tbl_4897 (LIKE tbl_447f);
+CREATE TABLE tbl_6978 (LIKE tbl_447f INCLUDING ENCRYPTED);
+
+\d+ tbl_4897
+\d+ tbl_6978
+
+CREATE VIEW view_3bc9 AS SELECT * FROM tbl_29f3;
+
+\d+ view_3bc9
+
+CREATE TABLE tbl_2386 AS SELECT * FROM tbl_29f3 WITH NO DATA;
+
+\d+ tbl_2386
+
+CREATE TABLE tbl_2941 AS SELECT * FROM tbl_29f3 WITH DATA;
+
+\d+ tbl_2941
+
+-- test partition declarations
+
+CREATE TABLE tbl_13fa (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+) PARTITION BY RANGE (a);
+CREATE TABLE tbl_13fa_1 PARTITION OF tbl_13fa FOR VALUES FROM (1) TO (100);
+
+\d+ tbl_13fa
+\d+ tbl_13fa_1
+
+
+-- test inheritance
+
+CREATE TABLE tbl_36f3_a (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_b (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1)
+);
+
+CREATE TABLE tbl_36f3_c (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek2)
+);
+
+CREATE TABLE tbl_36f3_d (
+    a int,
+    b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic)
+);
+
+CREATE TABLE tbl_36f3_e (
+    a int,
+    b text
+);
+
+-- not implemented (but could be ok)
+CREATE TABLE tbl_36f3_ab (c int) INHERITS (tbl_36f3_a, tbl_36f3_b);
+\d+ tbl_36f3_ab
+-- not implemented (but should fail)
+CREATE TABLE tbl_36f3_ac (c int) INHERITS (tbl_36f3_a, tbl_36f3_c);
+CREATE TABLE tbl_36f3_ad (c int) INHERITS (tbl_36f3_a, tbl_36f3_d);
+-- fail
+CREATE TABLE tbl_36f3_ae (c int) INHERITS (tbl_36f3_a, tbl_36f3_e);
+
+-- ok
+CREATE TABLE tbl_36f3_a_1 (b text ENCRYPTED WITH (column_encryption_key = cek1), c int) INHERITS (tbl_36f3_a);
+\d+ tbl_36f3_a_1
+-- fail
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek2), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text ENCRYPTED WITH (column_encryption_key = cek1, encryption_type = deterministic), c int) INHERITS (tbl_36f3_a);
+CREATE TABLE tbl_36f3_a_2 (b text, c int) INHERITS (tbl_36f3_a);
+
+DROP TABLE tbl_36f3_b, tbl_36f3_c, tbl_36f3_d, tbl_36f3_e;
+
+
+-- SET SCHEMA
+CREATE SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY cek1 SET SCHEMA test_schema_ce;
+ALTER COLUMN MASTER KEY cmk1 SET SCHEMA test_schema_ce;
+ALTER COLUMN ENCRYPTION KEY test_schema_ce.cek1 SET SCHEMA public;
+ALTER COLUMN MASTER KEY test_schema_ce.cmk1 SET SCHEMA public;
+DROP SCHEMA test_schema_ce;
+
+
+-- privileges
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- fail
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN MASTER KEY cmk1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE COLUMN ENCRYPTION KEY cek10 WITH VALUES (
+    column_master_key = cmk1,
+    algorithm = 'RSAES_OAEP_SHA_1',
+    encrypted_value = '\xDEADBEEF'
+);
+DROP COLUMN ENCRYPTION KEY cek10;
+
+-- fail
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+CREATE TABLE tbl_7040 (a int);
+-- fail
+ALTER TABLE tbl_7040 ADD COLUMN b text ENCRYPTED WITH (column_encryption_key = cek1);
+DROP TABLE tbl_7040;
+RESET SESSION AUTHORIZATION;
+GRANT USAGE ON COLUMN ENCRYPTION KEY cek1 TO regress_enc_user1;
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+-- ok now
+CREATE TABLE tbl_7040 (a int, b text ENCRYPTED WITH (column_encryption_key = cek1));
+RESET SESSION AUTHORIZATION;
+
+-- has_column_encryption_key_privilege
+SELECT has_column_encryption_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_colenckey WHERE cekname='cek1'), 'USAGE');
+SELECT has_column_encryption_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('cek1', 'USAGE');
+SELECT has_column_encryption_key_privilege('regress_enc_user1', 'cek1', 'USAGE');
+
+-- has_column_master_key_privilege
+SELECT has_column_master_key_privilege('regress_enc_user1',
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'),
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_colmasterkey WHERE cmkname='cmk1'), 'USAGE');
+SELECT has_column_master_key_privilege(
+    (SELECT oid FROM pg_roles WHERE rolname='regress_enc_user1'), 'cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('cmk1', 'USAGE');
+SELECT has_column_master_key_privilege('regress_enc_user1', 'cmk1', 'USAGE');
+
+DROP TABLE tbl_7040;
+REVOKE USAGE ON COLUMN ENCRYPTION KEY cek1 FROM regress_enc_user1;
+REVOKE USAGE ON COLUMN MASTER KEY cmk1 FROM regress_enc_user1;
+
+
+-- not useful here, just checking that it runs
+DISCARD COLUMN ENCRYPTION KEYS;
+
+
+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 SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- fail
+DROP COLUMN MASTER KEY cmk3;  -- fail
+RESET SESSION AUTHORIZATION;
+ALTER COLUMN MASTER KEY cmk3 OWNER TO regress_enc_user1;
+ALTER COLUMN ENCRYPTION KEY cek3 OWNER TO regress_enc_user1;
+\dcek cek3
+\dcmk cmk3
+SET SESSION AUTHORIZATION 'regress_enc_user1';
+DROP COLUMN ENCRYPTION KEY cek3;  -- ok now
+DROP COLUMN MASTER KEY cmk3;  -- ok now
+RESET SESSION AUTHORIZATION;
+
+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
+ALTER COLUMN ENCRYPTION KEY cek1 DROP VALUE (column_master_key = cmk1a, encrypted_value = '\xDEADBEEF');  -- fail
+
+DROP COLUMN ENCRYPTION KEY cek4;
+DROP COLUMN ENCRYPTION KEY fail;
+DROP COLUMN ENCRYPTION KEY IF EXISTS nonexistent;
+
+DROP COLUMN MASTER KEY cmk1a;
+DROP COLUMN MASTER KEY fail;
+DROP COLUMN MASTER KEY IF EXISTS nonexistent;
+
+DROP ROLE regress_enc_user1;
diff --git a/src/test/regress/sql/object_address.sql b/src/test/regress/sql/object_address.sql
index 1a6c61f49d5..61828613d9a 100644
--- a/src/test/regress/sql/object_address.sql
+++ b/src/test/regress/sql/object_address.sql
@@ -41,6 +41,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;
+CREATE COLUMN ENCRYPTION KEY addr_cek WITH VALUES (column_master_key = addr_cmk, algorithm = 'RSAES_OAEP_SHA_1', 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 (
@@ -93,6 +95,7 @@ CREATE STATISTICS addr_nsp.gentable_stat ON a, b FROM addr_nsp.gentable;
         ('materialized view'), ('foreign table'),
         ('table column'), ('foreign table column'),
         ('aggregate'), ('function'), ('procedure'), ('type'), ('cast'),
+        ('column encryption key'), ('column encryption key data'), ('column master key'),
         ('table constraint'), ('domain constraint'), ('conversion'), ('default value'),
         ('operator'), ('operator class'), ('operator family'), ('rule'), ('trigger'),
         ('text search parser'), ('text search dictionary'),
@@ -174,6 +177,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}', '{}'),
@@ -228,6 +234,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;
 
@@ -247,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
diff --git a/src/test/regress/sql/type_sanity.sql b/src/test/regress/sql/type_sanity.sql
index e88d6cbe49d..498ca654c07 100644
--- a/src/test/regress/sql/type_sanity.sql
+++ b/src/test/regress/sql/type_sanity.sql
@@ -546,6 +546,8 @@ CREATE TABLE tab_core_types AS SELECT
   WHERE oid < 16384 AND
     -- Exclude pseudotypes and composite types.
     typtype NOT IN ('p', 'c') AND
+    -- Exclude encryption internal types.
+    oid != ALL(ARRAY['pg_encrypted_det', 'pg_encrypted_rnd']::regtype[]) AND
     -- These reg* types cannot be pg_upgraded, so discard them.
     oid != ALL(ARRAY['regproc', 'regprocedure', 'regoper',
                      'regoperator', 'regconfig', 'regdictionary',
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c83417ce9d2..c6e9e025023 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -65,6 +65,8 @@ AllocSetFreeList
 AllocateDesc
 AllocateDescKind
 AlterCollationStmt
+AlterColumnEncryptionKeyStmt
+AlterColumnMasterKeyStmt
 AlterDatabaseRefreshCollStmt
 AlterDatabaseSetStmt
 AlterDatabaseStmt
@@ -378,6 +380,7 @@ CatCacheHeader
 CatalogId
 CatalogIdMapEntry
 CatalogIndexState
+CekInfo
 ChangeVarNodes_context
 ReplaceVarnoContext
 CheckPoint
@@ -403,6 +406,7 @@ ClusterInfo
 ClusterParams
 ClusterStmt
 CmdType
+CmkInfo
 CoalesceExpr
 CoerceParamHook
 CoerceToDomain
@@ -813,6 +817,9 @@ FormData_pg_authid
 FormData_pg_cast
 FormData_pg_class
 FormData_pg_collation
+FormData_pg_colenckey
+FormData_pg_colenckeydata
+FormData_pg_colmasterkey
 FormData_pg_constraint
 FormData_pg_conversion
 FormData_pg_database
@@ -1765,6 +1772,8 @@ PGAlignedBlock
 PGAlignedXLogBlock
 PGAsyncStatusType
 PGCALL2
+PGCEK
+PGCMK
 PGChecksummablePage
 PGContextVisibility
 PGEvent

base-commit: 5105c90796811f62711538155d207e5311eacf9b
-- 
2.44.0

#97Jelte Fennema-Nio
postgres@jeltef.nl
In reply to: Peter Eisentraut (#96)
Re: Transparent column encryption

On Wed, 10 Apr 2024 at 12:13, Peter Eisentraut <peter@eisentraut.org> wrote:

To kick some things off for PG18, here is an updated version of the
patch for automatic client-side column-level encryption.

I only read the docs and none of the code, but here is my feedback on
the current design:

(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.)

This seems like something that requires some more thought because CEK
rotation seems just as important as CMK rotation (often both would be
compromised at the same time). As far as I can tell the only way to
rotate a CEK is by re-encrypting the column for all rows in a single
go at the client side, thus taking a write-lock on all rows of the
table. That seems quite problematic, because that makes key rotation
an operation that requires application downtime. Allowing online
rotation is important, otherwise almost no-one will do it preventative
at a regular interval.

One way to allow online CEK rotation is by allowing a column to be
encrypted by one of several keys and/or allow a key to have multiple
versions. And then for each row we would store which key/version it
was encrypted with. That way for new insertions/updates clients would
use the newest version. But clients would still be able to decrypt
both old rows with the old key and new rows encrypted with the new
key, because the server would give them both keys and tell which row
was encrypted with which. Then the old rows can be rewritten by a
client in small batches, so that writes to the table can keep working
while this operation takes place.

This could even be used to allow encrypting previously unencrypted
columns using something like "ALTER COLUMN mycol ENCRYPTION KEY cek1".
Then unencrypted rows could be indicated by e.g. returning something
like NULL for the CEK.

+    The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.

It seems like it would be useful to have a way of storing the
plaintext in binary form too. I'm not saying this should be part of
the initial version, but it would be good to keep that in mind with
the design.

+ The session-specific identifier of the key.

Is it necessary for this identifier to be session-specific? Why not
use a global identifier like an oid? Anything session specific makes
the job of transaction poolers quite a bit harder. If this identifier
would be global, then the message can be forwarded as is to the client
instead of re-mapping identifiers between clients and servers (like is
needed for prepared statements).

+   Additional algorithms may be added to this protocol specification without a
+   change in the protocol version number.

What's the reason for not requiring a version bump for this?

+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each parameter:

It seems a little bit wasteful to include these for all columns, even
the ones that don't require encryption. How about only adding these
fields when format code is 0x11

Finally, I'm trying to figure out if this really needs to be a
protocol extension or if a protocol version bump would work as well
without introducing a lot of work for clients/poolers that don't care
about it (possibly with some modifications to the proposed protocol
changes). What makes this a bit difficult for me is that there's not
much written in the documentation on what is supposed to happen for
encrypted columns when the protocol extension is not enabled. Is the
content just returned/written like it would be with a bytea? Or is
writing disallowed because the format code would never be set to 0x11.

A related question to this is that currently libpq throws an error if
e.g. a master key realm is not defined but another one is. Is that
really what we want? Is not having one of the realms really that
different from not providing any realms at all?

But no-matter these behavioural details, I think it would be fairly
easy to add minimal "non-support" for this feature while supporting
the new protocol messages. All they would need to do is understand
what the new protocol messages/fields mean and either ignore them or
throw a clear error. For poolers it's a different story however. For
transaction pooling there's quite a bit of work to be done. I already
mentioned the session-specific ID being a problem, but even assuming
we change that to a global ID there's still difficulties. Key
information is only sent by the server if it wasn't sent before in the
session[1]+ When automatic client-side column-level 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., so a pooler would need to keep it's own cache and send
keys to clients that haven't received them yet.

So yeah, I think it would make sense to put this behind a protocol
extension feature flag, since it's fairly niche and would require
significant work at the pooler side to support.

[1]:
+    When automatic client-side column-level 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.
#98Peter Eisentraut
peter@eisentraut.org
In reply to: Jelte Fennema-Nio (#97)
Re: Transparent column encryption

On 10.04.24 16:14, Jelte Fennema-Nio wrote:

(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.)

This seems like something that requires some more thought because CEK
rotation seems just as important as CMK rotation (often both would be
compromised at the same time).

Hopefully, the reason for key rotation is mainly that policies require
key rotation, not that keys get compromised all the time. That's the
reason for having this two-tier key system in the first place. This
seems pretty standard to me. For example, I can change the password on
my laptop's file system encryption, which somehow wraps a lower-level
key, but I can't reencrypt the actual file system in place.

+    The plaintext inside
+    the ciphertext is always in text format, but this is invisible to the
+    protocol.

It seems like it would be useful to have a way of storing the
plaintext in binary form too. I'm not saying this should be part of
the initial version, but it would be good to keep that in mind with
the design.

Two problems here: One, for deterministic encryption, everyone needs to
agree on the representation, otherwise equality comparisons won't work.
Two, if you give clients the option of storing text or binary, then
clients also get back a mix of text or binary, and it will be a mess.
Just giving the option of storing the payload in binary wouldn't be that
hard, but it's not clear what you can sensibly do with that in the end.

+ The session-specific identifier of the key.

Is it necessary for this identifier to be session-specific? Why not
use a global identifier like an oid? Anything session specific makes
the job of transaction poolers quite a bit harder. If this identifier
would be global, then the message can be forwarded as is to the client
instead of re-mapping identifiers between clients and servers (like is
needed for prepared statements).

The point was just to avoid saying specifically that the OID will be
sent, because then that would tie the catalog representation to the
protocol, which seems unnecessary. Maybe we can reword that somehow.

In terms of connection pooling, this feature as it is conceived right
now would only work in session pooling anyway. Even if the identifiers
somehow were global (but OIDs can also change and are not guaranteed
unique forever), the state of which keys have already been sent is
session state.

+   Additional algorithms may be added to this protocol specification without a
+   change in the protocol version number.

What's the reason for not requiring a version bump for this?

This is kind of like SASL or TLS can add new methods dynamically without
requiring a new version. I mean, as we are learning, making new
protocol versions is kind of hard, so the point was to avoid it.

+      If the protocol extension <literal>_pq_.column_encryption</literal> is
+      enabled (see <xref linkend="protocol-flow-column-encryption"/>), then
+      there is also the following for each parameter:

It seems a little bit wasteful to include these for all columns, even
the ones that don't require encryption. How about only adding these
fields when format code is 0x11

I guess you could do that, but wouldn't that making the decoding of
these messages much more complicated? You would first have to read the
"short" variant, decode the format, and then decide to read the rest.
Seems weird.

Finally, I'm trying to figure out if this really needs to be a
protocol extension or if a protocol version bump would work as well
without introducing a lot of work for clients/poolers that don't care
about it (possibly with some modifications to the proposed protocol
changes).

That's not something this patch cares about, but the philosophical
discussions in the other thread on protocol versioning etc. appear to
lean toward protocol extension.

What makes this a bit difficult for me is that there's not
much written in the documentation on what is supposed to happen for
encrypted columns when the protocol extension is not enabled. Is the
content just returned/written like it would be with a bytea?

Yes, that's what would happen, and that's the intention, so that for
example you can use pg_dump to back up encrypted columns without having
to decrypt them.

A related question to this is that currently libpq throws an error if
e.g. a master key realm is not defined but another one is. Is that
really what we want? Is not having one of the realms really that
different from not providing any realms at all?

Can you provide a more concrete example of what scenario you have a
concern about?

#99Jelte Fennema-Nio
postgres@jeltef.nl
In reply to: Peter Eisentraut (#98)
Re: Transparent column encryption

On Thu, 18 Apr 2024 at 13:25, Peter Eisentraut <peter@eisentraut.org> wrote:

Hopefully, the reason for key rotation is mainly that policies require
key rotation, not that keys get compromised all the time.

These key rotation policies are generally in place to reduce the
impact of a key compromise by limiting the time a compromised key is
valid.

This
seems pretty standard to me. For example, I can change the password on
my laptop's file system encryption, which somehow wraps a lower-level
key, but I can't reencrypt the actual file system in place.

I think the threat model for this proposal and a laptop's file system
encryption are different enough that the same choices/tradeoffs don't
automatically translate. Specifically in this proposal the unencrypted
CEK is present on all servers that need to read/write those encrypted
values. And a successful attacker would then be able to read the
encrypted values forever with this key, because it effectively cannot
be rotated. That is a much bigger attack surface and risk than a
laptop's disk encryption. So, I feel quite strongly that shipping the
proposed feature without being able to re-encrypt columns in an online
fashion would be a mistake.

That's the
reason for having this two-tier key system in the first place.

If we allow for online-rotation of the actual encryption key, then
maybe we don't even need this two-tier system ;)

Not having this two tier system would have a few benefits in my opinion:
1. We wouldn't need to be sending encrypted key material from the
server to every client. Which seems nice from a security, bandwidth
and client implementation perspective.
2. Asymmetric encryption of columns is suddenly an option. Allowing
certain clients to enter encrypted data into the database but not read
it.

Two problems here: One, for deterministic encryption, everyone needs to
agree on the representation, otherwise equality comparisons won't work.
Two, if you give clients the option of storing text or binary, then
clients also get back a mix of text or binary, and it will be a mess.
Just giving the option of storing the payload in binary wouldn't be that
hard, but it's not clear what you can sensibly do with that in the end.

How about defining at column creation time if the underlying value
should be binary or not? Something like:

CREATE TABLE t(
mytime timestamp ENCRYPTED WITH (column_encryption_key = cek1, binary=true)
);

Even if the identifiers
somehow were global (but OIDs can also change and are not guaranteed
unique forever),

OIDs of existing rows can't just change while a connection is active,
right? (all I know is upgrades can change them but that seems fine)
Also they are unique within a catalog table, right?

the state of which keys have already been sent is
session state.

I agree that this is the case. But it's state that can be tracked
fairly easily by a transaction pooler. Similar to how prepared
statements can be tracked. And this is easier to do when at the IDs of
the same keys are the same across each session to the server, because
if they differ then you need to do re-mapping of IDs.

This is kind of like SASL or TLS can add new methods dynamically without
requiring a new version. I mean, as we are learning, making new
protocol versions is kind of hard, so the point was to avoid it.

Fair enough

I guess you could do that, but wouldn't that making the decoding of
these messages much more complicated? You would first have to read the
"short" variant, decode the format, and then decide to read the rest.
Seems weird.

I see your point. But with the current approach even for queries that
don't return any encrypted columns, these useless fields would be part
of the RowDescryption. It seems quite annoying to add extra network
and parsing overhead all of your queries even if only a small
percentage use the encryption feature. Maybe we should add a new
message type instead like EncryptedRowDescription, or add some flag
field at the start of RowDescription that can be used to indicate that
there is encryption info for some of the columns.

Yes, that's what would happen, and that's the intention, so that for
example you can use pg_dump to back up encrypted columns without having
to decrypt them.

Okay, makes sense. But I think it would be good to document that.

A related question to this is that currently libpq throws an error if
e.g. a master key realm is not defined but another one is. Is that
really what we want? Is not having one of the realms really that
different from not providing any realms at all?

Can you provide a more concrete example of what scenario you have a
concern about?

A server has table A and B. A is encrypted with a master key realm X
and B is encrypted with master key realm Y. If libpq is only given a
key for realm X, and it then tries to read table B, an error is
thrown. While if you don't provide any realm at all, you can read from
table B just fine, only you will get bytea fields back.

#100Robert Haas
robertmhaas@gmail.com
In reply to: Peter Eisentraut (#96)
Re: Transparent column encryption

On Wed, Apr 10, 2024 at 6:13 AM Peter Eisentraut <peter@eisentraut.org> wrote:

Obviously, it's early days, so there will be plenty of time to have
discussions on various other aspects of this patch. I'm keeping a keen
eye on the discussion of protocol extensions, for example.

I think the way that you handled that is clever, and along the lines
of what I had in mind when I invented the _pq_ stuff.

More specifically, the way that the ColumnEncryptionKey and
ColumnMasterKey messages are handled is exactly the way that I was
imagining things would work. The client uses _pq_.column_encryption to
signal that it can understand those messages, and the server responds
by including them. I assume that if the client doesn't signal
understanding, then the server simply omits sending those messages. (I
have not checked the code.)

I'm less certain about the changes to the ParameterDescription and
RowDescription messages. I see a couple of potential problems. One is
that, if you say you can understand column encryption messages, the
extra fields are included even for unencrypted columns. The client
must choose at connection startup whether it ever wishes to read any
encrypted data; if so, it pays a portion of that overhead all the
time. Another potential problem is with the scalability of this
design. Suppose that we could not only encrypt columns, but also
compress, fold, mutilate, and spindle them. Then there might end up
being a dizzying array of variation in the format of what is supposed
to be the same message. Perhaps it's not so bad: as long as the
documentation is clear about in which order the additional fields will
appear in the relevant messages when more than one relevant feature is
used, it's probably not too difficult for clients to cope. And it is
probably also true that the precise size of, say, a RowDescription
message will rarely be performance-critical. But another thought is
that we might try to redesign this so that we simply add more message
types rather than mutating message types i.e. after sending the
RowDescription message, if any columns are encrypted, we additionally
send a RowEncryptionDescription message. Then this treatment becomes
symmetric with the handling of ColumnEncryptionKey and ColumnMasterKey
messages, and there's no overhead when the feature is unused.

With regard to the Bind message, I suggest that we regard the protocol
change as reserving a currently-unused bit in the message to indicate
whether the value is pre-encrypted, without reference to the protocol
extension. It could be legal for a client that can't understand
encryption message from the server to supply an encrypted value to be
inserted into a column. And I don't think we would ever want the bit
that's being reserved here to be used by some other extension for some
other purpose, even when this extension isn't used. So I don't see a
need for this to be tied into the protocol extension.

--
Robert Haas
EDB: http://www.enterprisedb.com

#101Jelte Fennema-Nio
postgres@jeltef.nl
In reply to: Robert Haas (#100)
Re: Transparent column encryption

On Thu, 18 Apr 2024 at 18:46, Robert Haas <robertmhaas@gmail.com> wrote:

With regard to the Bind message, I suggest that we regard the protocol
change as reserving a currently-unused bit in the message to indicate
whether the value is pre-encrypted, without reference to the protocol
extension. It could be legal for a client that can't understand
encryption message from the server to supply an encrypted value to be
inserted into a column. And I don't think we would ever want the bit
that's being reserved here to be used by some other extension for some
other purpose, even when this extension isn't used. So I don't see a
need for this to be tied into the protocol extension.

I think this is an interesting idea. I can indeed see use cases for
e.g. inserting a new row based on another row (where the secret is the
same).

IMHO that means that we should also bump the protocol version for this
change, because it's changing the wire protocol by adding a new
parameter format code. And it does so in a way that does not depend on
the new protocol extension.

#102Robert Haas
robertmhaas@gmail.com
In reply to: Jelte Fennema-Nio (#101)
Re: Transparent column encryption

On Thu, Apr 18, 2024 at 1:49 PM Jelte Fennema-Nio <postgres@jeltef.nl> wrote:

I think this is an interesting idea. I can indeed see use cases for
e.g. inserting a new row based on another row (where the secret is the
same).

IMHO that means that we should also bump the protocol version for this
change, because it's changing the wire protocol by adding a new
parameter format code. And it does so in a way that does not depend on
the new protocol extension.

I think we're more or less covering the same ground we did on the
other thread here -- in theory I don't love the fact that we never
bump the protocol version when we change stuff, but in practice if we
start bumping it every time we do anything I think it's going to just
break a bunch of stuff without any real benefit.

--
Robert Haas
EDB: http://www.enterprisedb.com

#103Dave Cramer
davecramer@postgres.rocks
In reply to: Robert Haas (#100)
Re: Transparent column encryption

On Thu, 18 Apr 2024 at 12:46, Robert Haas <robertmhaas@gmail.com> wrote:

On Wed, Apr 10, 2024 at 6:13 AM Peter Eisentraut <peter@eisentraut.org>
wrote:

Obviously, it's early days, so there will be plenty of time to have
discussions on various other aspects of this patch. I'm keeping a keen
eye on the discussion of protocol extensions, for example.

I think the way that you handled that is clever, and along the lines
of what I had in mind when I invented the _pq_ stuff.

More specifically, the way that the ColumnEncryptionKey and
ColumnMasterKey messages are handled is exactly the way that I was
imagining things would work. The client uses _pq_.column_encryption to
signal that it can understand those messages, and the server responds
by including them. I assume that if the client doesn't signal
understanding, then the server simply omits sending those messages. (I
have not checked the code.)

I'm less certain about the changes to the ParameterDescription and
RowDescription messages. I see a couple of potential problems. One is
that, if you say you can understand column encryption messages, the
extra fields are included even for unencrypted columns. The client
must choose at connection startup whether it ever wishes to read any
encrypted data; if so, it pays a portion of that overhead all the
time. Another potential problem is with the scalability of this
design. Suppose that we could not only encrypt columns, but also
compress, fold, mutilate, and spindle them. Then there might end up
being a dizzying array of variation in the format of what is supposed
to be the same message. Perhaps it's not so bad: as long as the
documentation is clear about in which order the additional fields will
appear in the relevant messages when more than one relevant feature is
used, it's probably not too difficult for clients to cope. And it is
probably also true that the precise size of, say, a RowDescription
message will rarely be performance-critical. But another thought is
that we might try to redesign this so that we simply add more message
types rather than mutating message types i.e. after sending the
RowDescription message, if any columns are encrypted, we additionally
send a RowEncryptionDescription message. Then this treatment becomes
symmetric with the handling of ColumnEncryptionKey and ColumnMasterKey
messages, and there's no overhead when the feature is unused.

With regard to the Bind message, I suggest that we regard the protocol
change as reserving a currently-unused bit in the message to indicate
whether the value is pre-encrypted, without reference to the protocol
extension. It could be legal for a client that can't understand
encryption message from the server to supply an encrypted value to be
inserted into a column. And I don't think we would ever want the bit
that's being reserved here to be used by some other extension for some
other purpose, even when this extension isn't used. So I don't see a
need for this to be tied into the protocol extension.

--
Robert Haas
EDB: http://www.enterprisedb.com

I just picked this thread up so apologies if this has already been
discussed.

Instead of sending the information about encrypted columns back in the
DESCRIBE message I have been contemplating returning that information back
in the PARSECOMPLETE message. I"ve thought about this for other things as
well. The JDBC driver has to do a round trip to describe after we parse a
named statement. Seems to me that returning the DESCRIBE immediately would
avoid this round trip.

Dave