Skip to content

feat: check column configuration against encrypted column before decrypt #231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
29 changes: 29 additions & 0 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [Unknown column](#encrypt-unknown-column)
- [Unknown table](#encrypt-unknown-table)
- [Unknown index term](#encrypt-unknown-index-term)
- [Column configuration mismatch](#encrypt-column-config-mismatch)

- Decrypt errors:
- [Column could not be deserialised](#encrypt-column-could-not-be-deserialised)
Expand Down Expand Up @@ -392,6 +393,34 @@ Unknown Index Term for column '{column_name}' in table '{table_name}'.


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


## Column configuration mismatch <a id='encrypt-column-config-mismatch'></a>

A returned encrypted column does not match the column configuration.

### Error message

```
Column configuration for column '{column_name}' in table '{table_name}' does not match the encrypted column.
```

### Notes

CipherStash Proxy validates that encrypted columns match the configuration before decrypting any data.
If the table and column are not the same, this error is returned.
The check is there to help prevent "confused deputy" issues and the error should *never* appear during normal operation.

If the error persists, please contact CipherStash [support](https://cipherstash.com/support).


### Further reading

[AWS: The confused deputy problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html)
[Wikipedia: Confused deputy problem](https://en.wikipedia.org/wiki/Confused_deputy_problem)

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



Expand Down
2 changes: 1 addition & 1 deletion docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ This will output the version of EQL installed.
In your existing PostgreSQL database, you store your data in tables and columns.
Those columns have types like `integer`, `text`, `timestamp`, and `boolean`.
When storing encrypted data in PostgreSQL with Proxy, you use a special column type called `eql_v1_encrypted`, which is [provided by EQL](#setting-up-the-database-schema).
`eql_v1_encrypted` is a container column type that can be used for any type of encrypted data you want to store or search, whether they are numbers (`int`, `small_int`, `big_int`), text (`text`), dates and times (`date`), or booleans (`boolean`).
`eql_v1_encrypted` is a container column type that can be used for any type of encrypted data you want to store or search, whether they are numbers (`int`, `small_int`, `big_int`), text (`text`), dates and times (`date`. `timestamp`), or booleans (`boolean`).

Create a table with an encrypted column for `email`:

Expand Down
3 changes: 3 additions & 0 deletions packages/cipherstash-proxy/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ pub enum EncryptError {
#[error("Column '{column}' in table '{table}' could not be encrypted. For help visit {}#encrypt-column-could-not-be-encrypted", ERROR_DOC_BASE_URL)]
ColumnCouldNotBeEncrypted { table: String, column: String },

#[error("Column configuration for column '{column}' in table '{table}' does not match the encrypted column. For help visit {}#encrypt-column-config-mismatch", ERROR_DOC_BASE_URL)]
ColumnConfigurationMismatch { table: String, column: String },

/// This should in practice be unreachable
#[error("Missing encrypt configuration for column type `{plaintext_type}`. For help visit {}#encrypt-missing-encrypt-configuration", ERROR_DOC_BASE_URL)]
MissingEncryptConfiguration { plaintext_type: String },
Expand Down
40 changes: 38 additions & 2 deletions packages/cipherstash-proxy/src/postgresql/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use super::message_buffer::MessageBuffer;
use super::messages::error_response::ErrorResponse;
use super::messages::row_description::RowDescription;
use super::messages::BackendCode;
use super::Column;
use crate::connect::Sender;
use crate::encrypt::Encrypt;
use crate::eql::EqlEncrypted;
use crate::error::Error;
use crate::log::{DEVELOPMENT, MAPPER, PROTOCOL};
use crate::error::{EncryptError, Error};
use crate::log::{DECRYPT, DEVELOPMENT, MAPPER, PROTOCOL};
use crate::postgresql::context::Portal;
use crate::postgresql::messages::data_row::DataRow;
use crate::postgresql::messages::param_description::ParamDescription;
Expand Down Expand Up @@ -270,6 +271,8 @@ where

let start = Instant::now();

self.check_column_config(projection_columns, &ciphertexts)?;

// Decrypt CipherText -> Plaintext
let plaintexts = self.encrypt.decrypt(ciphertexts).await.inspect_err(|_| {
counter!(DECRYPTION_ERROR_TOTAL).increment(1);
Expand Down Expand Up @@ -313,6 +316,39 @@ where
Ok(())
}

fn check_column_config(
&mut self,
projection_columns: &[Option<Column>],
ciphertexts: &[Option<EqlEncrypted>],
) -> Result<(), Error> {
for (col, ct) in projection_columns.iter().zip(ciphertexts) {
match (col, ct) {
(Some(col), Some(ct)) => {
if col.identifier != ct.identifier {
return Err(EncryptError::ColumnConfigurationMismatch {
table: col.identifier.table.to_owned(),
column: col.identifier.column.to_owned(),
}
.into());
}
}
// configured column with NULL ciphertext
(Some(_), None) => {}
// unconfigured column *should* have no ciphertext,
(None, None) => {}
// ciphertext with no column configuration is bad
(None, Some(ct)) => {
return Err(EncryptError::ColumnConfigurationMismatch {
table: ct.identifier.table.to_owned(),
column: ct.identifier.column.to_owned(),
}
.into());
}
}
}
Ok(())
}

async fn parameter_description_handler(
&self,
bytes: &BytesMut,
Expand Down