Skip to content
Closed
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
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@
/cipherstash-proxy.local.toml
mise.local.toml
tests/pg/data**
tests/sql/cipherstash-encrypt.sql
tests/sql/cipherstash-encrypt-uninstall.sql
.vscode

rust-toolchain.toml
.cargo/config.toml

# release artifacts
/cipherstash-proxy
/cipherstash-eql.sql
/packages/cipherstash-proxy/eql-version-at-build-time.txt
/cipherstash-encrypt.sql
/cipherstash-encrypt-uninstall.sql

# credentials for local dev
.env.proxy.docker
Expand Down
4 changes: 2 additions & 2 deletions docs/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ For example:

## Unknown Column <a id='encrypt-unknown-column'></a>

The column has an encrypted type (PostgreSQL `cs_encrypted_v1` type ) with no encryption configuration.
The column has an encrypted type (PostgreSQL `eql_v1_encrypted` type ) with no encryption configuration.

Without the configuration, Cipherstash Proxy does not know how to encrypt the column.
Any data is unprotected and unencrypted.
Expand All @@ -341,7 +341,7 @@ Column 'column_name' in table 'table_name' has no Encrypt configuration

## Unknown Table <a id='encrypt-unknown-table'></a>

The table has one or more encrypted columns (PostgreSQL `cs_encrypted_v1` type ) with no encryption configuration.
The table has one or more encrypted columns (PostgreSQL `eql_v1_encrypted` type ) with no encryption configuration.

Without the configuration, Cipherstash Proxy does not know how to encrypt the column.
Any data is unprotected and unencrypted.
Expand Down
8 changes: 4 additions & 4 deletions docs/getting-started/schema-example.sql
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
TRUNCATE TABLE cs_configuration_v1;
TRUNCATE TABLE public.eql_v1_configuration;

-- Exciting cipherstash table
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id SERIAL PRIMARY KEY,
encrypted_email cs_encrypted_v1,
encrypted_dob cs_encrypted_v1,
encrypted_salary cs_encrypted_v1
encrypted_email eql_v1_encrypted,
encrypted_dob eql_v1_encrypted,
encrypted_salary eql_v1_encrypted
);

SELECT cs_add_index_v1(
Expand Down
10 changes: 5 additions & 5 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ You can also install EQL by running [the installation script](https://github.com
Once you have installed EQL, you can see what version is installed by querying the database:

```sql
SELECT cs_eql_version();
SELECT eql_v1.version();
```

This will output the version of EQL installed.
Expand All @@ -162,22 +162,22 @@ 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 `cs_encrypted_v1`, which is [provided by EQL](#setting-up-the-database-schema).
`cs_encrypted_v1` 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`).
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`).

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

```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email cs_encrypted_v1
email eql_v1_encrypted
)
```

This creates a `users` table with two columns:

- `id`, an autoincrementing integer column that is the primary key for the record
- `email`, a `cs_encrypted_v1` column
- `email`, a `eql_v1_encrypted` column

There are important differences between the plaintext columns you've traditionally used in PostgreSQL and encrypted columns with CipherStash Proxy:

Expand Down
37 changes: 18 additions & 19 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -409,27 +409,28 @@ fi
"""

[tasks."postgres:setup"]
depends = ["postgres:eql:teardown"]
alias = 's'
description = "Installs EQL and applies schema to database"
run = """
#!/bin/bash
cd tests
mise run postgres:fail_if_not_running
mise run postgres:eql:download
cat sql/cipherstash-encrypt.sql | docker exec -i postgres${CONTAINER_SUFFIX} psql postgresql://${CS_DATABASE__USERNAME}:${CS_DATABASE__PASSWORD}@${CS_DATABASE__HOST}:${CS_DATABASE__PORT}/${CS_DATABASE__NAME} -f-
cat sql/schema-uninstall.sql | docker exec -i postgres${CONTAINER_SUFFIX} psql postgresql://${CS_DATABASE__USERNAME}:${CS_DATABASE__PASSWORD}@${CS_DATABASE__HOST}:${CS_DATABASE__PORT}/${CS_DATABASE__NAME} -f-
cat ../cipherstash-encrypt-uninstall.sql | docker exec -i postgres${CONTAINER_SUFFIX} psql postgresql://${CS_DATABASE__USERNAME}:${CS_DATABASE__PASSWORD}@${CS_DATABASE__HOST}:${CS_DATABASE__PORT}/${CS_DATABASE__NAME} -f-
cat ../cipherstash-encrypt.sql | docker exec -i postgres${CONTAINER_SUFFIX} psql postgresql://${CS_DATABASE__USERNAME}:${CS_DATABASE__PASSWORD}@${CS_DATABASE__HOST}:${CS_DATABASE__PORT}/${CS_DATABASE__NAME} -f-
cat sql/schema.sql | docker exec -i postgres${CONTAINER_SUFFIX} psql postgresql://${CS_DATABASE__USERNAME}:${CS_DATABASE__PASSWORD}@${CS_DATABASE__HOST}:${CS_DATABASE__PORT}/${CS_DATABASE__NAME} -f-
"""

[tasks."postgres:eql:teardown"]
alias = 's'
depends = ["eql:download"]
description = "Uninstalls EQL and removes schema from database"
run = """
#!/bin/bash
cd tests
mise run postgres:fail_if_not_running
mise run postgres:eql:download
cat sql/schema-uninstall.sql | docker exec -i postgres${CONTAINER_SUFFIX} psql postgresql://${CS_DATABASE__USERNAME}:${CS_DATABASE__PASSWORD}@${CS_DATABASE__HOST}:${CS_DATABASE__PORT}/${CS_DATABASE__NAME} -f-
cat sql/cipherstash-encrypt-uninstall.sql | docker exec -i postgres${CONTAINER_SUFFIX} psql postgresql://${CS_DATABASE__USERNAME}:${CS_DATABASE__PASSWORD}@${CS_DATABASE__HOST}:${CS_DATABASE__PORT}/${CS_DATABASE__NAME} -f-
cat ../cipherstash-encrypt-uninstall.sql | docker exec -i postgres${CONTAINER_SUFFIX} psql postgresql://${CS_DATABASE__USERNAME}:${CS_DATABASE__PASSWORD}@${CS_DATABASE__HOST}:${CS_DATABASE__PORT}/${CS_DATABASE__NAME} -f-
"""

[tasks."postgres:up"]
Expand Down Expand Up @@ -490,34 +491,32 @@ for d in tests/pg/data-*; do
done
"""


[tasks."postgres:eql:download"]
[tasks."eql:download"]
alias = 'e'
description = "Download latest EQL release"
description = "Download latest EQL release or use local copy"
dir = "{{config_root}}/tests"
outputs = [
"{{config_root}}/tests/sql/cipherstash-encrypt.sql",
"{{config_root}}/tests/sql/cipherstash-encrypt-uninstall.sql",
"{{config_root}}/cipherstash-encrypt.sql",
"{{config_root}}/cipherstash-encrypt-uninstall.sql",
]
run = """
# install script
if [ -z "$CS_EQL_PATH" ]; then
curl -sLo sql/cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/download/${CS_EQL_VERSION}/cipherstash-encrypt.sql
curl -sLo "{{config_root}}/cipherstash-encrypt.sql" https://github.com/cipherstash/encrypt-query-language/releases/download/${CS_EQL_VERSION}/cipherstash-encrypt.sql
else
echo "Using EQL: ${CS_EQL_PATH}"
cp "$CS_EQL_PATH" sql/cipherstash-encrypt.sql
echo "Using EQL: ${CS_EQL_PATH}/cipherstash-encrypt.sql"
cp "$CS_EQL_PATH/cipherstash-encrypt.sql" "{{config_root}}/cipherstash-encrypt.sql"
fi

# uninstall script
if [ -z "$CS_EQL_UNINSTALL_PATH" ]; then
curl -sLo sql/cipherstash-encrypt-uninstall.sql https://github.com/cipherstash/encrypt-query-language/releases/download/${CS_EQL_VERSION}/cipherstash-encrypt-uninstall.sql
if [ -z "$CS_EQL_PATH" ]; then
curl -sLo "{{config_root}}/cipherstash-encrypt-uninstall.sql" https://github.com/cipherstash/encrypt-query-language/releases/download/${CS_EQL_VERSION}/cipherstash-encrypt-uninstall.sql
else
echo "Using EQL: ${CS_EQL_PATH}"
cp "$CS_EQL_UNINSTALL_PATH" sql/cipherstash-encrypt-uninstall.sql
echo "Using EQL: ${CS_EQL_PATH}/cipherstash-encrypt-uninstall.sql"
cp "$CS_EQL_PATH/cipherstash-encrypt-uninstall.sql" "{{config_root}}/cipherstash-encrypt-uninstall.sql"
fi
"""


[tasks."python:test"]
dir = "{{config_root}}/tests"
description = "Runs python tests"
Expand Down Expand Up @@ -567,7 +566,7 @@ cp -v {{config_root}}/target/{{ target }}/release/cipherstash-proxy {{config_roo
"""

[tasks."build:docker"]
depends = ["build:docker:fetch_eql"]
depends = ["eql:download"]
description = "Build a Docker image for cipherstash-proxy"
run = """
{% set default_platform = "linux/" ~ arch() | replace(from="x86_64", to="amd64") %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ mod tests {
let msg = err.to_string();

// This is similar to below. The error message comes from tokio-postgres when Proxy
// returns cs_encrypted_v1 and the client cannot convert to a string.
// returns eql_v1_encrypted and the client cannot convert to a string.
// If mapping errors are enabled (enable_mapping_errors or CS_DEVELOPMENT__ENABLE_MAPPING_ERRORS),
// then Proxy will return an error that says "Column X in table Y has no Encrypt configuration"
assert_eq!(msg, "error serializing parameter 1: cannot convert between the Rust type `&str` and the Postgres type `cs_encrypted_v1`");
assert_eq!(msg, "error serializing parameter 1: cannot convert between the Rust type `&str` and the Postgres type `eql_v1_encrypted`");
} else {
unreachable!();
}
Expand Down
3 changes: 1 addition & 2 deletions packages/cipherstash-proxy/src/encrypt/config/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,7 @@ pub async fn load_encrypt_config(config: &DatabaseConfig) -> Result<EncryptConfi
}
}
}

fn configuration_table_not_found(e: &tokio_postgres::Error) -> bool {
let msg = e.to_string();
msg.contains("cs_configuration_v1") && msg.contains("does not exist")
msg.contains("eql_v1_configuration") && msg.contains("does not exist")
}
5 changes: 3 additions & 2 deletions packages/cipherstash-proxy/src/encrypt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@ impl Encrypt {

let eql_version = {
let client = connect::database(&config.database).await?;
let rows = client.query("SELECT cs_eql_version();", &[]).await;
let rows = client.query("SELECT eql_v1.version() AS version;", &[]).await;
// let rows = client.query("SELECT 'WAT' AS version;", &[]).await;

match rows {
Ok(rows) => rows.first().map(|row| row.get("cs_eql_version")),
Ok(rows) => rows.first().map(|row| row.get("version")),
Err(err) => {
warn!(
msg = "Could not query EQL version from database",
Expand Down
11 changes: 5 additions & 6 deletions packages/cipherstash-proxy/src/encrypt/schema/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,19 +132,18 @@ pub async fn load_schema(config: &DatabaseConfig) -> Result<Schema, Error> {
let table_name: String = table.get("table_name");
let primary_keys: Vec<Option<String>> = table.get("primary_keys");
let columns: Vec<String> = table.get("columns");
let _types: Vec<Option<String>> = table.get("column_types");
let domains: Vec<Option<String>> = table.get("column_domains");
let column_type_names: Vec<Option<String>> = table.get("column_type_names");

let mut table = Table::new(Ident::new(&table_name));

columns.iter().zip(domains).for_each(|(col, domain)| {
columns.iter().zip(column_type_names).for_each(|(col, column_type_name)| {
let is_primary_key = primary_keys.contains(&Some(col.to_string()));

let ident = Ident::with_quote('"', col);

let column = match domain.as_deref() {
Some("cs_encrypted_v1") => {
debug!(target: SCHEMA, msg = "cs_encrypted_v1 column", table = table_name, column = col);
let column = match column_type_name.as_deref() {
Some("eql_v1_encrypted") => {
debug!(target: SCHEMA, msg = "eql_v1_encrypted column", table = table_name, column = col);
Column::eql(ident)
}
_ => Column::native(ident),
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
SELECT data FROM cs_configuration_v1 WHERE state = 'active' LIMIT 1;
SELECT data FROM public.eql_v1_configuration WHERE state = 'active' LIMIT 1;
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ SELECT
t.table_name,
array_agg(distinct k.column_name)::text[] AS primary_keys,
array_agg(c.column_name)::text[] AS columns,
array_agg(c.data_type)::text[] AS column_types,
array_agg(c.domain_name)::text[] AS column_domains
array_agg(c.udt_name)::text[] AS column_type_names
FROM
information_schema.tables t
LEFT JOIN
Expand All @@ -24,3 +23,6 @@ GROUP BY
t.table_schema, t.table_name
ORDER BY
t.table_schema, t.table_name;



8 changes: 4 additions & 4 deletions packages/cipherstash-proxy/src/postgresql/messages/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ impl Parse {
}

///
/// Encrypted columns are the cs_encrypted_v1 Domain Type
/// cs_encrypted_v1 wraps JSONB
/// Encrypted columns are the eql_v1_encrypted Domain Type
/// eql_v1_encrypted wraps JSONB
///
/// Using JSONB to avoid the complexity of loading the OID of cs_encrypted_v1
/// PostgreSQL will coerce JSONB to cs_encrypted_v1 if it passes the constaint check
/// Using JSONB to avoid the complexity of loading the OID of eql_v1_encrypted
/// PostgreSQL will coerce JSONB to eql_v1_encrypted if it passes the constaint check
///
pub fn rewrite_param_types(&mut self, columns: &[Option<Column>]) {
for (idx, col) in columns.iter().enumerate() {
Expand Down
58 changes: 47 additions & 11 deletions packages/eql-mapper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1017,27 +1017,63 @@ mod test {
)]
);

let transformed_statement = match typed.transform(HashMap::from_iter([(
match typed.transform(HashMap::from_iter([(
typed.literals[0].1.as_node_key(),
ast::Value::SingleQuotedString("ENCRYPTED".into()),
)])) {
Ok(transformed_statement) => transformed_statement,
Ok(transformed_statement) => assert_eq!(
transformed_statement.to_string(),
"SELECT * FROM employees WHERE salary > ROW('ENCRYPTED'::JSONB)"
),
Err(err) => panic!("statement transformation failed: {}", err),
};
}

#[test]
fn insert_with_literal_subsitution() {
// init_tracing();

let schema = resolver(schema! {
tables: {
employees: {
id,
salary (EQL),
}
}
});

let statement = parse(
r#"
insert into employees (salary) values (20000)
"#,
);

// This type checks the transformed statement so we can get hold of the encrypted literal.
let typed = match type_check(schema, &transformed_statement) {
let typed = match type_check(schema.clone(), &statement) {
Ok(typed) => typed,
Err(err) => panic!("type check failed: {:#?}", err),
};

assert!(typed.literals.contains(&(
EqlValue(TableColumn {
table: id("employees"),
column: id("salary")
}),
&ast::Value::SingleQuotedString("ENCRYPTED".into()),
)));
assert_eq!(
typed.literals,
vec![(
EqlValue(TableColumn {
table: id("employees"),
column: id("salary")
}),
&ast::Value::Number(20000.into(), false)
)]
);

match typed.transform(HashMap::from_iter([(
typed.literals[0].1.as_node_key(),
ast::Value::SingleQuotedString("ENCRYPTED".into()),
)])) {
Ok(transformed_statement) => assert_eq!(
transformed_statement.to_string(),
"INSERT INTO employees (salary) VALUES (ROW('ENCRYPTED'::JSONB))"
),
Err(err) => panic!("statement transformation failed: {}", err),
};
}

#[test]
Expand Down
Loading
Loading