From 81cff57b9294bc3f9d5d2887e0bdc86762470a66 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Thu, 15 May 2025 16:53:23 +1000 Subject: [PATCH] feat: check column configuration against encrypted column before decrypt --- docs/errors.md | 29 ++++++++++++++ docs/how-to.md | 2 +- packages/cipherstash-proxy/src/error.rs | 3 ++ .../src/postgresql/backend.rs | 40 ++++++++++++++++++- 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/docs/errors.md b/docs/errors.md index 8bc1f669..1b7eebf5 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -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) @@ -392,6 +393,34 @@ Unknown Index Term for column '{column_name}' in table '{table_name}'. + + + +## Column configuration mismatch + +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) + + diff --git a/docs/how-to.md b/docs/how-to.md index 5f906a84..562500a7 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -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`: diff --git a/packages/cipherstash-proxy/src/error.rs b/packages/cipherstash-proxy/src/error.rs index ad3762af..96621f35 100644 --- a/packages/cipherstash-proxy/src/error.rs +++ b/packages/cipherstash-proxy/src/error.rs @@ -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 }, diff --git a/packages/cipherstash-proxy/src/postgresql/backend.rs b/packages/cipherstash-proxy/src/postgresql/backend.rs index 8722b765..b423630f 100644 --- a/packages/cipherstash-proxy/src/postgresql/backend.rs +++ b/packages/cipherstash-proxy/src/postgresql/backend.rs @@ -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; @@ -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); @@ -313,6 +316,39 @@ where Ok(()) } + fn check_column_config( + &mut self, + projection_columns: &[Option], + ciphertexts: &[Option], + ) -> 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,