Query logging would tell you what queries were executed.
E.g the following would tell you that 10 user records were returned with name and email but not which users.
SELECT name, email FROM users LIMIT 10;
CipherStash can tell you the ID of every row that was returned, like:
If you need to know the actual values instead of just the IDs (say when doing an investigation), you can do a query to join the audit data with the users table.
Assuming audit logs are in a table called audit with a column called record_id and a timestamp:
-- full forensic analysis
SELECT name,email FROM audit
LEFT JOIN users ON users.id = audit.record_id
WHERE timestamp BETWEEN $1 AND $2;
And because the data is encrypted, that query will ALSO be audited. Not even admins escape the auditing :p
All of this is important for "materiality" investigations. If you suspect some data has been accessed inappropriately, knowing how much and exactly which records is very useful - especially if deciding if customer/regulator notification is required.
> What does the risk profile look like in a full leak of the encrypted database?
We actually have a number of different schemes which trade leakage for performance/storage overhead/compatibility. Each one has its own type in Postgres so you just create a column with whatever type you want.
The weakest (most leakage) is OPE - order preserving encryption. We use an encoding scheme that works like scientific notation so an attacker with a DB snapshot would learn relative order but not the size of the gaps between values. Its major benefit is compatibility - it works everywhere.
The strongest is Block ORE - Order Revealing Encryption. A DB snapshot reveals nothing more than standard randomized encryption (IND-CPA2 semantic security).
The tradeoff is that BlockORE values are much bigger: 32-bit integer goes to 384 bytes. On Postgres its still very fast and works with standard B-trees.
> If you had a column of integers are you able to order them without decrypting?
Yes. For either scheme, its just an EQL function:
SELECT * FROM foo ORDER by eql_v3.ord_term(x);
> Check that values are the same?
-- $1: encrypt(query)
SELECT * FROM foo WHERE eql_v3.eq_term(x) = eql_v3.eq_term($1);
> Identify null values?
NULL is just like any value for OPE/ORE - encrypt it and use that to query
-- $1: encrypt(NULL)
SELECT * FROM foo WHERE eql_v3.eq_term(x) = eql_v3.eq_term($1);
NULL values can be encrypted with either scheme. Very safe to do so with the ORE scheme (fully randomized values). If encrypting strings, NULLs would reveal length but you can always pad if not leaking value length is important to your security model.
In the OPE scheme, NULL would encrypt deterministically so if you expect a lot of NULLs in your data this could leak the distribution.
We are working on a capability now called Leakage Tuned Domains. The goal here is not to eliminate leakage but scope it to a specific data type (e.g. birthdays) such that any leakage cannot be an advantage to an attacker. Think k-anonymisation or differential-privacy on steroids.
Applications do not have access - individual authenticated users have access. A user must be authenticated & authorised in order to be able to create encrypted query terms. This is enforced by CipherStash's key server (ZeroKMS), not the application.
Having a copy of the production database and a local copy of the application and a debugger is insufficient to be able to encrypt/decrypt any values or run any queries involving encrypted search terms: you'd still need to authenticate with ZeroKMS.
Having root access to a production server running an application is a different matter because you'd have access to the entire address space of the app that real authenticated users are interacting with. A sufficiently motivated adversary would be able to intercept auth tokens allowing impersonation of an authorised user.
The application server process is a trust boundary in this model and thus requires appropriate access controls.
To address this last point, we now support decryption in edge workers like Cloudflare or Supabase Edge Functions. We also have browser-based encryption/decryption in the works. Both of these approaches mean data is never decrypted in the app, not even in production.
CipherStash isn't something that we just threw together on a weekend. We've spent years developing it. I've personally invested almost every waking moment for the last 6 years working on this (and the prior 2 preparing to quit my job so I could work on it full time).
I get that it seems like snake oil to some. But that's just because we don't do a good enough job of explaining what we've created. We're working on that - we're engineers, not marketers.
There have been prior attempts at making tech like this work, and maybe because those approaches were limited, its tainted how folks see encryption tech. We've studied many of the previous attempts and invested in the engineering required to overcome the problems.
Key management - we made a 2 party key system that is faster and has a better security model than AWS KMS alone. Not because we thought it was fun, not on a whim but because it was the only way to solve the real security challenges we faced when it comes to protecting data.
Searchable encryption - that is less secure than homomorphic encryption but FAST. Like under a few milliseconds for most queries (homomorphic would take literally minutes or worse). And a whole lot better than no encryption at all. It uses a key per value.
Encrypt query language - a framework that makes searchable encryption work in standard postgres. No exotic index schemes, no custom builds. Just SQL functions.
Typescript and Rust libraries to make this _almost_ drop-in for Prisma Next, supabase-js
and Drizzle.
Identity integrations with Supabase, Clerk, Auth0, and Okta.
A postgres proxy that automatically encrypts and decrypts data for authorized requests - for when using the SDK isn't possible/convenient.
And an audit system that records every access and can send data to anything you can connect to AWS firehose or an S3 bucket.
This is real and IMHO is exceptional engineering...anyone know a great technical marketer?!
This doesn’t have anything to do with being able to change the code. Devs can code and change things as they need. The database is also the same as it always is.
What CipherStash does is let you specify specific columns that you want to encrypt. The application (via our SDK), encrypts values for that field before it’s saved and optionally decrypts it again when it’s read. Decryption is performed when the user provides a valid credential (JWT).
This would allow you to give your devs full access to the production DB. You don’t have to give them permission to decrypt any or specific fields if they don’t need it.
So for your use case that would possibly save a lot of effort - no need to create a santized copy of the database (which in itself is fraught).
What James was referring to was the attacker threat model. Decryption happens in the application code. So an attacker who can steal encryption keys as well as be a valid JWT would be able to decrypt data for as long as the JWT is valid. That scenario is not something CipherStash can fully prevent.
What it does prevent is unauthorised access to data. Whether via the database directly or via the application.
What’s important here is that it’s the data itself that protected. If you move encrypted values from your prod DB to test or dev, the same rules apply. A user who can’t read “email” values that were encrypted in prod would still not be able to read those values if you moved them somewhere else.
No, to derive a key you need a client key (controlled by the app) and key-seeds for each value which can only be retrieved from the key server with a valid JWT. The JWT is time bound (15 mins).
Now, if an attacker could gain access to the client key AND a valid JWT then they would be able to decrypt until the JWT expired. Blast radius reduced but we don't stop the attack entirely. An attacker with that much advantage is pretty hard to stop dead. But note that 2 things are required for the adversary to do this - where as in most systems, getting a JWT or a decryption key alone would be enough.
If you're not a cryptographer, then I'd suggest you reserve judgement about what flaws you think might be in the system. We have cryptographers on our team, have connections with universities in the US and Australia and our work is based on published, peer-reviewed papers. E.g. Lewi-Wu 2016 and Syalim et al 2011 and 2021.
No, not by default. You could but as you said, that would be a A LOT of data.
It depends on your setup. If you're using Supabase, one way is to send the logs to Clickhouse and use the Clickhouse partner integration to query the audit logs and join it to the actual data.
Keeping only the ids in the audit log means you need to stitch the data together later. This means you don't accidentally leak data via your audit trail!
Explanation:
The identifier is actually for the key that encrypts the value (1 unique key per value).
1. When the value is encrypted for the first time, it gets an ID.
2. When its decrypted, the application requests the key for that ID from the key-server (key-server records that the data was accessed)*
3. When updating, the same data key is used so the ID is persistent*
* Technically the key server doesn't return a key, it generates partial key material that can be used to derive the data key in the app. It can do this at up to 10,000 keys per second.
* You can also tag each value with the table/column name and the row-id to link everything together
The 3 values form the "descriptor" of a value: `table/column/id`.
CipherStash - specifically the key service. There is a lot of data for sure but we only record an identifier for each value and (optionally) the user ID. It compresses well.
Keys are not stored in the database or in the application.
Every data key is derived at query time via a 2-party system:
1. by the key server which manages root-key material (stored in an HSM or traditional KMS)
2. keys derived via a user credential at the time of the query
SQL Injection case:
An adversary signs up to your app, pulls of a SQLi and you might think that the app will just decrypt the values. It won't because the adversary's auth cred can't derive keys to decrypt anything but the data they are explicitly allowed to access.
RCE/shell compromise:
Because keys are derived on demand these kinds of attacks are largely inert. If the attacker could read memory from the running app on-demand then they could conceivably see credentials in flight but each key only ever decrypts one value so the blast radius is limited.
On the cost of decryption:
Queries typically only return a subset of the values in a table and, importantly, don't need to decrypt anything at all. Querying and decryption are independent.
If you DO need to decrypt a large volume of data, CipherStash can do it at 10,000 values per second.
Query performance is under 1ms for simple queries and no-more than 200-300ms for more complex queries.
No security tool is perfect, nor "gold plated" but this tech meaningfully reduces blast area of attacks, makes many attacks inert and gives you an incredibly reliable audit trail.
Searchable encryption means you can have encryption and queries (from apps etc) still work for authorized users.
This is all started when I was the CTO of a health-tech and the engineers all had access to patient data. They needed DB access (data migrations, managing backups, reading non-sensitive fields) but not access to the sensitive values. 10m+ patient records - it made me nervous.
Row-level Security (RLS) was a nightmare, it was slow, easy to mess up and could be overridden by DBAs anyway. Plus the application connected to the DB using a service account so RLS was useless for end-user access control.
CipherStash solves the following:
* Hide sensitive data from direct database accessors (authorized or not!)
My original use-case. Direct access to the DB doesn't reveal sensitive data (unless the user also has permission to decrypt a specific record).
* Support or even replace application access controls
Decryption happens in the application layer and is based on end-user identity. That means a gap in application defenses is mitigated by the encryption. If the user can't decrypt the value then they can't access the data.
* Access auditing
Every sensitive data access is recorded (and by who) which is very useful for compliance and incident response. SQL query auditing is great for knowing what queries were run but it doesn't tell you what data was actually returned. Its also blind once data actually leaves the DB and can inadvertently leak sensitive data itself. Using key accesses with non-identifying value IDs to audit decryptions makes the auditing more reliable and super granular.
We have customers who use the tech to prove to their users (often large enterprise) that no internal staff can access the data.
* Agent access to data
The encryption shifts access control decisions down to the data layer on a per-value basis. This is incredibly useful for gating access to agents that need access to data. Every decryption requires authentication (e.g via Supabase Auth, Auth0 etc) and combining it with agent identities (creds issued to agents) means you can not only limit what agents can access, you can also audit exactly what _was_ accessed.
The searchable encryption is the enabler: value level encryption is what makes all of this possible. Searchable encryption means you can have encryption and queries (from apps etc) still work for authorized users.
Hope that helps! I'm so in the weeds I don't know if I explain things that well sometimes!