Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

- **Connection settings in transactions**: `SET CIPHERSTASH.KEYSET_ID`, `KEYSET_NAME` and `UNSAFE_DISABLE_MAPPING` now follow PostgreSQL's rules for settings in transactions. A change, including a `RESET`, `SET ... TO DEFAULT` or `DISCARD ALL`, takes effect once PostgreSQL confirms it, is kept on `COMMIT`, and is undone on `ROLLBACK` or `ROLLBACK TO SAVEPOINT`, or when the transaction fails, including the implicit transaction of a simple query or extended-protocol batch. `SET LOCAL` now lasts only until the end of the transaction. Previously Proxy kept these settings outside PostgreSQL's transactions: a rolled-back reset left Proxy with no keyset, so later writes were encrypted under `default_keyset_id` instead of the selected keyset; a rolled-back `SET CIPHERSTASH.UNSAFE_DISABLE_MAPPING = true` left mapping disabled; and `SET LOCAL` was treated as a plain `SET`. Setting names are now matched as PostgreSQL matches them, ignoring case and quoting, and `UNSAFE_DISABLE_MAPPING` accepts every PostgreSQL boolean spelling. A `SET CIPHERSTASH.*` that Proxy cannot honour, such as an unknown setting name or a value it cannot interpret, is now refused with an error instead of being ignored. A simple query that Proxy rewrites now forwards every statement in it; previously statements Proxy did not map, such as a `SET`, could be dropped from a query that also encrypted values. A prepared statement is now refused with SQLSTATE `26000` if mapping has been turned on or off since it was prepared or bound, or if the keyset has changed and it holds values encrypted under the previous keyset, for example after a rollback undid the change it was prepared under; prepare it again to run it under the new settings. A statement or message that Proxy refuses now fails the batch, and an open transaction, in PostgreSQL as well, as an error from PostgreSQL would. Previously Proxy answered a refused extended-protocol batch itself, so statements earlier in the batch could still commit and the client's transaction carried on as if nothing had failed. A statement pipelined after a query or batch that may change these settings now waits for PostgreSQL to answer it, so it is mapped under the settings PostgreSQL confirmed. Previously Proxy assumed the change succeeded, so a `SET` that failed, or a `SET LOCAL` that ended with its implicit transaction, still applied to the statements sent after it.

- **`COPY ... FROM STDIN`**: Proxy now forwards a `COPY ... FROM STDIN` into a table with no encrypted columns, with its copy data, so `psql`'s `\copy` and driver `COPY` APIs can bulk-load plaintext tables through Proxy. Previously Proxy refused the statement as invalid SQL unless it ended with `;`, with or without an encryption configuration. A `COPY ... FROM` into an encrypted column is now refused with an error that names the column, as Proxy does not encrypt `COPY` data; previously a `COPY` from a file or program was forwarded with its plaintext. When encrypted columns are configured, a `COPY ... FROM` into a table or column that Proxy has not loaded, or into a table name that more than one schema has, is refused, as Proxy cannot tell whether it is encrypted. Statements after a `COPY ... FROM STDIN;` in the same simple query are now checked and encrypted like any other; previously Proxy read them as copy data and forwarded them unchanged, so a plaintext `INSERT` into an encrypted column could reach PostgreSQL. A simple query that has a `COPY` and a statement Proxy encrypts is refused, and the `COPY` must be sent in a query of its own. A `COPY (INSERT ... RETURNING ...) TO STDOUT`, or a `COPY` of another query that writes, is refused when encrypted columns are configured; previously Proxy forwarded its values as plaintext. Proxy now parses every `COPY`, so it refuses as invalid SQL the few `COPY` options its SQL parser does not support, such as `WHERE`, `HEADER MATCH` and `ON_ERROR`.

## [2.2.4] - 2026-06-18

### Fixed
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

105 changes: 105 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
- [Invalid SQL statement](#mapping-invalid-sql-statement)
- [Unsupported parameter type](#mapping-unsupported-parameter-type)
- [Statement could not be type checked](#mapping-statement-could-not-be-type-checked)
- [COPY into encrypted column](#mapping-copy-into-encrypted-column)
- [COPY into unknown table](#mapping-copy-into-unknown-table)
- [COPY of a query that writes](#mapping-copy-of-query-that-writes)
- [COPY with a statement that uses an encrypted column](#mapping-copy-with-encrypted-statement)
- [Internal Error](#mapping-internal-error)
- [Prepared settings changed](#mapping-prepared-settings-changed)

Expand Down Expand Up @@ -245,6 +249,107 @@ If the error persists, please contact CipherStash [support](https://cipherstash.



<!-- ---------------------------------------------------------------------------------------------------- -->


## COPY into encrypted column <a id='mapping-copy-into-encrypted-column'></a>

A `COPY ... FROM` statement would load data into an encrypted column.
CipherStash Proxy does not encrypt `COPY` data, so it refuses the statement rather than send plaintext to the encrypted column.

### Error message

```
COPY into encrypted column 'encrypted_email' of table 'users' is not supported, as Proxy does not encrypt COPY data.
```

### Notes

A `COPY` with no column list loads every column of the table, so it is refused when the table has any encrypted column.
A `COPY ... FROM` into a table with no encrypted columns, or into only the plaintext columns of a table, is forwarded to PostgreSQL with its data.
A `COPY ... TO` of a table or a query that reads is forwarded, and returns the encrypted columns as ciphertext, as Proxy does not decrypt `COPY` data.
Proxy checks a `COPY` with the SQL parser, so it refuses as invalid SQL the few `COPY` options the parser does not support, such as `WHERE`, `HEADER MATCH` and `ON_ERROR`, even with no encryption configured.

### How to fix

Load the encrypted columns with `INSERT` statements through CipherStash Proxy, which encrypts the values.
Use `COPY` only for columns that are not encrypted.


<!-- ---------------------------------------------------------------------------------------------------- -->


## COPY into unknown table <a id='mapping-copy-into-unknown-table'></a>

A `COPY ... FROM` statement would load data into a table or column that is not in the database schema CipherStash Proxy has loaded.
Proxy cannot tell whether the data would go to an encrypted column, so it refuses the statement.

### Error message

```
COPY into table 'users' is not supported, as Proxy cannot tell which of its columns are encrypted.
```

### Notes

Proxy loads the tables of every schema by name, without the schema.
A table name used in more than one schema is refused, as Proxy cannot tell which table is meant.
A table created on the same connection, earlier or in the same query, is known by the name it was created with, so `CREATE TABLE public.staging` is known to `COPY public.staging` but not to `COPY staging`.
A `COPY` is refused if either a table created with that name or the loaded table of that name has an encrypted column it would fill, as Proxy cannot tell whether PostgreSQL created the table, such as after `CREATE TABLE IF NOT EXISTS` or a rollback.
So a `COPY` into a temporary table named like an encrypted table is refused.
A `COPY` with no column list into a table whose columns Proxy does not know, such as one created with `CREATE TABLE ... AS`, is refused.
Any `COPY` into a table created with `CREATE TABLE ... (LIKE ...)` is refused, as Proxy does not know which columns it copies.
When no columns are encrypted, Proxy does not check the table of a `COPY`.

### How to fix

Check that the table and columns exist.
If the table was created on another connection, retry once Proxy has reloaded the database schema.
If the table name is used in more than one schema, rename one of the tables.


<!-- ---------------------------------------------------------------------------------------------------- -->


## COPY of a query that writes <a id='mapping-copy-of-query-that-writes'></a>

A `COPY (...) TO` statement has a query that writes, such as `INSERT ... RETURNING`, `UPDATE ... RETURNING` or `DELETE ... RETURNING`, or a `WITH` query that does.
CipherStash Proxy does not encrypt the query of a `COPY`, so it refuses the statement rather than send its values to PostgreSQL as plaintext.

### Error message

```
COPY of a query that writes is not supported, as Proxy does not encrypt the query of a COPY.
```

### Notes

When no columns are encrypted, Proxy forwards the `COPY`.

### How to fix

Run the statement on its own, without `COPY`, so Proxy encrypts it.


<!-- ---------------------------------------------------------------------------------------------------- -->


## COPY with a statement that uses an encrypted column <a id='mapping-copy-with-encrypted-statement'></a>

A simple query contains a `COPY` together with a statement that CipherStash Proxy encrypts.
Proxy rewrites such a query to encrypt it, and cannot keep the `COPY` in the rewritten query.

### Error message

```
COPY must be sent in a query of its own when the query has a statement that uses an encrypted column.
```

### How to fix

Send the `COPY` in a query of its own.


<!-- ---------------------------------------------------------------------------------------------------- -->


Expand Down
1 change: 1 addition & 0 deletions packages/cipherstash-proxy-integration/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ cipherstash-config = { workspace = true }
cipherstash-proxy = { path = "../cipherstash-proxy/" }
chrono = { version = "0.4.39", features = ["clock"] }
fake = { version = "4", features = ["chrono", "derive"] }
futures-util = { version = "0.3", features = ["sink"] }
hex = "0.4.3"
postgres-types = { version = "0.2.9", features = ["derive"] }
rand = "0.9"
Expand Down
66 changes: 66 additions & 0 deletions packages/cipherstash-proxy-integration/src/copy_from_stdin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! `COPY ... FROM STDIN` through Proxy, which tokio-postgres sends with the extended protocol.
//! Proxy forwards a copy into a table with no encrypted columns, and refuses one into an
//! encrypted column, as it does not encrypt copy data.

#[cfg(test)]
mod tests {
use crate::common::{clear, connect_with_tls, random_id, trace, PROXY};
use bytes::Bytes;
use futures_util::SinkExt;
use std::pin::pin;

#[tokio::test]
async fn copy_into_a_plaintext_table() {
trace();
clear().await;
let client = connect_with_tls(PROXY).await;

let (first, second) = (random_id(), random_id());
let mut sink = pin!(client
.copy_in("COPY plaintext (id, plaintext) FROM STDIN")
.await
.unwrap());
sink.send(Bytes::from(format!("{first}\talice\n{second}\tbob\n")))
.await
.unwrap();
assert_eq!(sink.as_mut().finish().await.unwrap(), 2);

let rows = client
.query(
"SELECT plaintext FROM plaintext WHERE id = ANY($1) ORDER BY plaintext",
&[&vec![first, second]],
)
.await
.unwrap();
let copied: Vec<String> = rows.iter().map(|row| row.get(0)).collect();
assert_eq!(copied, vec!["alice", "bob"]);
}

#[tokio::test]
async fn copy_into_an_encrypted_column_is_refused() {
trace();
clear().await;
let client = connect_with_tls(PROXY).await;

let err = client
.copy_in::<_, Bytes>("COPY encrypted (id, encrypted_text) FROM STDIN")
.await
.err()
.unwrap();
assert!(err.to_string().contains(
"COPY into encrypted column 'encrypted_text' of table 'encrypted' is not supported"
));

// A COPY with no column list fills every column, including the encrypted ones
let err = client
.copy_in::<_, Bytes>("COPY encrypted FROM STDIN")
.await
.err()
.unwrap();
assert!(err.to_string().contains("COPY into encrypted column"));

// The connection is still usable
let rows = client.simple_query("SELECT 1").await.unwrap();
assert!(!rows.is_empty());
}
}
1 change: 1 addition & 0 deletions packages/cipherstash-proxy-integration/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod common;
mod connection_resilience;
mod copy_from_stdin;
mod decrypt;
mod diagnostics;
mod disable_mapping;
Expand Down
12 changes: 12 additions & 0 deletions packages/cipherstash-proxy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ pub enum MappingError {
#[error("Statement could not be type checked: {}. For help visit {}#mapping-statement-could-not-be-type-checked", _0, ERROR_DOC_BASE_URL)]
StatementCouldNotBeTypeChecked(String),

#[error("COPY into encrypted column '{column}' of table '{table}' is not supported, as Proxy does not encrypt COPY data. For help visit {}#mapping-copy-into-encrypted-column", ERROR_DOC_BASE_URL)]
CopyIntoEncryptedColumn { table: String, column: String },

#[error("COPY into table '{table}' is not supported, as Proxy cannot tell which of its columns are encrypted. For help visit {}#mapping-copy-into-unknown-table", ERROR_DOC_BASE_URL)]
CopyIntoUnknownTable { table: String },

#[error("COPY of a query that writes is not supported, as Proxy does not encrypt the query of a COPY. For help visit {}#mapping-copy-of-query-that-writes", ERROR_DOC_BASE_URL)]
CopyOfQueryThatWrites,

#[error("COPY must be sent in a query of its own when the query has a statement that uses an encrypted column. For help visit {}#mapping-copy-with-encrypted-statement", ERROR_DOC_BASE_URL)]
CopyWithEncryptedStatement,

#[error("Statement could not be transformed: {0}")]
StatementCouldNotBeTransformed(String),

Expand Down
Loading
Loading