Column Encryption
Overview
Support for client-side encryption of data was added in version 3.20.0 of the C# driver. When using this feature data will be encrypted on-the-fly according to a provided implementation of IColumnEncryptionPolicy
. This policy is also used to decrypt data in returned rows. If a prepared statement is used, this decryption is transparent to the user; retrieved data will be decrypted and converted into the original
type according to definitions in the encryption policy. Support for simple (i.e. non-prepared) statements is also available, although in this case values must be wrapped using the EncryptedValue
type.
Client-side encryption and decryption should work against all versions of Cassandra, DSE, and Astra. It does not utilize any server-side functionality to do its work.
Configuration
Client-side encryption is enabled by creating an instance of an implementation of the IColumnEncryptionPolicy
interface and adding information about columns to be encrypted to it. This policy is then supplied to the Builder
via the WithColumnEncryptionPolicy
method.
var policy = new AesColumnEncryptionPolicy();
var cluster =
Cluster.Builder()
.AddContactPoint("127.0.0.1")
.WithColumnEncryptionPolicy(policy)
.Build();
AESColumnEncryptionPolicy
AESColumnEncryptionPolicy
is an implementation of IColumnEncryptionPolicy
which provides encryption and decryption via AES-128, AES-192, or AES-256 according to the size of the key that is provided. This class is currently the only available column encryption policy implementation, although users can certainly implement their own by implementing IColumnEncryptionPolicy
or subclassing the abstract BaseColumnEncryptionPolicy
class which provides some out of the box functionality to manage the encrypted column metadata.
To mark a column as “encrypted” users need to call the AddColumn
method. This method has two overloads, one of them has an additional IColumnInfo
parameter which should only be used if the column has a type of list
, set
, map
, udt
, tuple
or custom
. If you are using one of these six types on that particular column then you need to provide a IColumnInfo
object with a type that matches the cql type (e.g. list
requires a ListColumnInfo
object to be provided in the AddColumn
method).
You need to provide the keyspace, table, and column names using the AddColumn
method. Additionally, you need to provide a key for each column.
The key type for this specific policy (i.e. AESColumnEncryptionPolicy
) is AesKeyAndIV
. You can create objects of this type using the constructors. The key is mandatory but the IV is optional:
var keyOnly = new AesColumnEncryptionPolicy.AesKeyAndIV(key, iv);
var keyAndIv = new AesColumnEncryptionPolicy.AesKeyAndIV(key);
If you don’t provide an IV then a new one will be generated each time an encryption operation occurs. The final encrypted value contains the IV so that the driver can read this IV during read requests (before decrypting). This also means that the driver can discard the newly generated IV as soon as the encryption operation is done.
Using a newly generated IV on each encryption operation also means that the same input will result in a different server side value everytime an encryption occurs. This is important if the column is part of the primary key or an index for example.
If the encrypted column is part of a primary key, index, or WHERE
clause of a statement, ensure that the encrypted value at the server side is always the same for the same input value. This requires you to provide a “static” IV instead of relying on the policy to generate a new one for every encryption.
This is an example of a table with two encrypted columns:
CREATE TABLE IF NOT EXISTS ks.table(id blob, address blob, public_notes text, PRIMARY KEY(id))
Our two encrypted columns are id
and address
(note the blob
types, the server only sees encrypted bytes).
var base64key = "__BASE64_ENCODED_KEY__";
var base64IV = "__BASE64_ENCODED_IV__";
// key for the id column which is the primary key (static IV)
var idKey = new AesColumnEncryptionPolicy.AesKeyAndIV(Convert.FromBase64String(base64key), Convert.FromBase64String(base64IV));
// key for the address column which is not used in any WHERE clause, primary key or index (no IV is provided - more secure)
var addressKey = new AesColumnEncryptionPolicy.AesKeyAndIV(Convert.FromBase64String(base64key));
Then, add these columns to the policy:
var policy = new AesColumnEncryptionPolicy();
policy.AddColumn("ks", "table", "id", idKey, ColumnTypeCode.Uuid);
policy.AddColumn("ks", "table", "address", addressKey, ColumnTypeCode.Text);
var cluster =
Cluster.Builder()
.AddContactPoint("127.0.0.1")
.WithColumnEncryptionPolicy(policy)
.Build();
We added the id
column with type uuid
and address
with type text
. Avoid altering the types specified in the policy once you begin writing data to the columns. Changing the types afterward could result in a column containing two different types of encrypted data, creating compatibility problems for the driver. There is no server validation of these types because this is a client side feature. These types only have meaning at the client side, and the server only sees the blob
data. The encryption key is also never sent to the server.
Usage
Encryption
Prepared Statements
Client-side encryption is most effective when used with prepared statements. A prepared statement is aware of information about the columns in the query it was built from. We can use this information to transparently encrypt any supplied parameters. For example, we can create a prepared statement to insert a value into id
by executing the following code after creating a Cluster
in the manner described above:
var insertPs = await _session.PrepareAsync("INSERT INTO ks.table (id, address, public_notes) VALUES (?, ?, ?)").ConfigureAwait(false);
var userId = Guid.NewGuid();
var address = "Street X";
var publicNotes = "Public notes 1.";
var boundInsert = insertPs.Bind(userId, address, publicNotes);
await _session.ExecuteAsync(boundInsert).ConfigureAwait(false);
Our encryption policy will detect that “id” is an encrypted column and take appropriate action.
Simple Statements
Client-side encryption can also be used with simple queries, although such use cases are certainly not transparent. The driver provides an EncryptedValue
struct that you can use to mark a specific parameter as “encrypted”:
var userId = Guid.NewGuid();
var address = "Street X";
var publicNotes = "Public notes 1.";
// using encrypted columns with SimpleStatements require the parameters to be wrapped with the EncryptedValue type
var insert = new SimpleStatement(
"INSERT INTO ks.table (id, address, public_notes) VALUES (?, ?, ?)",
new EncryptedValue(userId, idKey),
new EncryptedValue(address, addressKey),
publicNotes);
await _session.ExecuteAsync(insert).ConfigureAwait(false);
Note that creating an instance of type EncryptedValue
requires the key to be provided. This key is the same that was added to the policy via the AddColumn
method for that particular column.
Decryption
Decryption of values returned from the server is always transparent. Whether we’re executing a simple or prepared statement, encrypted columns will be decrypted automatically and made available via rows just like any other result.
Example
The examples directory at the driver’s Github repository has an example for column encryption.
Limitations
Aliases
Using aliases when selecting encrypted columns is not supported because the driver will see the alias as the column name. This causes it not to match the column name that was configured in the encryption policy.
E.g.:
CREATE TABLE ks.table (encrypted blob PRIMARY KEY)
// this works normally
SELECT encrypted FROM ks.table;
// this will not work, the driver will not attempt to decrypt the column
SELECT encrypted as e FROM ks.table;
var policy = new AesColumnEncryptionPolicy();
policy.AddColumn("ks", "table", "encrypted", KEY, TYPECODE);
Named parameters
If you use a named parameter instead of ?
on a prepared statement, the driver will only be able to detect that the parameter should be encrypted if the name of the parameter matches the name of the column. This is essentially the same limitation as the “aliases” one above but there’s a work around for this one: you can treat it as a Simple Statement and wrap the parameter value in an instance of the EncryptedValue
type to tell the driver that the parameter should be encrypted.
CREATE TABLE ks.table (encrypted blob PRIMARY KEY)
// both of these 2 work normally
INSERT INTO ks.table (encrypted) VALUES (?);
INSERT INTO ks.table (encrypted) VALUES (:encrypted);
// this requires the value to be wrapped by an instance of the EncryptedValue class
INSERT INTO ks.table (encrypted) VALUES (:e);
var insert = await session.PrepareAsync("INSERT INTO ks.table (encrypted) VALUES (:e)").ConfigureAwait(false);
var insertBoundStatement = insert.Bind(new EncryptedValue(VALUE, KEY));
await session.ExecuteAsync(insertBoundStatement).ConfigureAwait(false);
Custom column encryption policy
If the provided AesColumnEncryptionPolicy
does not suit your needs, use a custom implementation of IColumnEncryptionPolicy
. There are two ways of doing this:
- Sub class
BaseColumnEncryptionPolicy
- Implement
IColumnEncryptionPolicy
directly
BaseColumnEncryptionPolicy
is an abstract class that provides some out of the box utility code to manage the encrypted column metadata (e.g. AddColumn
method). It implements IColumnEncryptionPolicy.GetColumnEncryptionMetadata(string ks, string table, string col)
so you only have to implement the actual encryption and decryption logic by overriding the EncryptWithKey
and DecryptWithKey
methods.
Note that BaseColumnEncryptionPolicy
has a type parameter. This will be the type of the key that has to be provided when adding columns with the AddColumn
method. It is also the type of the key that is provided in EncryptWithKey
and DecryptWithKey
. This provides some type safety on top of the base IColumnEncryptionPolicy
interface which declares the key type to be just object
. For example, the AesColumnEncryptionPolicy
uses the custom type AesKeyAndIV
as the key type.
If BaseColumnEncryptionPolicy
does not suit your needs, you can implement IColumnEncryptionPolicy
directly. However, you will have to implement a way to manage the column encryption metadata in order to implement the IColumnEncryptionPolicy.GetColumnEncryptionMetadata
method.