Security is, was, and always will be, of paramount concern to software developers. Ideally, we build our applications with multiple layers of security, and typically, encryption plays a key role in our efforts to protect sensitive data (passwords, credit card numbers, etc.) against unauthorized access. With the advent of cloud computing, security concerns become even more pressing, because we need to draw a clear separation between those who own our data (us, the client), from those who manage it (our cloud service provider). This is where Always Encrypted – a new security feature in SQL Server 2016 – comes into play.
Older Encryption Features
Before diving in to Always Encrypted, let’s first establish some context by reviewing existing encryption features that have been available in earlier versions of SQL Server. For one, column level encryption has been available since SQL Server 2005. This type of encryption uses either certificates or symmetric keys such that, without the required certificate or key, encrypted data remains hidden from view.
As of SQL Server 2008 (Enterprise Edition), you can also use Transparent Data Encryption (TDE) to encrypt the entire database – again, using a special certificate and database encryption key – such that without the required certificate and key, the entire database remains encrypted and completely inaccessible. A really nice feature of TDE is that any backup files made of the database are also rendered unusable without the TDE certificate and database encryption key.
From a security standpoint, these older features do serve us well. However, they suffer from two significant drawbacks. First, the very certificates and keys used for encryption are themselves stored in the database (or database server), which means that the database is always capable of decrypting the data (as long as the required certificates and keys are present). While this may have been perfectly fine some years back, it becomes a major problem today if you are interested in moving their data to the cloud. By giving up physical ownership of the database, you are handing over the encryption keys and certificates to your cloud provider, empowering them to access your private data.
Another issue is the fact that these older features only encrypt data “at rest” – meaning, data is only encrypted when it is being written to disk. So even if, for example, SSL is used to encrypt a credit card number between the client browser and the web server, it then gets sent in clear text over the wire from the web server to the database server, where it ultimately gets encrypted once more in database storage. And the same is true in the opposite direction, when reading back the credit card. So, between the web server and the database server, a hacker could sniff out your sensitive data. Again, this was perhaps a lesser concern years ago, but nowadays customers also want to move their applications and virtual networks to the cloud as well, not just their databases. Yet migrating to the cloud also means handing over the sensitive network connection between the web and database servers, that used to be in your control when that infrastructure was hosted in your own data centers.
And so now, in SQL Server 2016 (all editions), we have Always Encrypted, where “always” really means all the time. So data is encrypted not just at rest, but also in flight. Furthermore, the encryption keys themselves – which are essential for both encrypting and decrypting – are not stored in the database. Those keys stay with you, the client. And that’s why, as you’ll see, Always Encrypted is a hybrid feature, where all the encryption and decryption occurs exclusively on the client side.
With this approach, you effectively separate the clients who own the data, from the cloud providers who manage it. Because the data is always encrypted, SQL Server is powerless to decrypt it. All it can do is serve it up in its encrypted state, so that the data is also encrypted in flight. Only when it arrives at the client can it be decrypted, on the client, by the client, who possesses the necessary keys. Likewise, when inserting or updating new data, that data gets encrypted immediately on the client, before it ever leaves the client, and remains encrypted in-flight all the way to the database server, where SQL Server can only store it in that encrypted state – it cannot decrypt it. This is classic GIGO (garbage in, garbage out), as far as SQL Server is concerned.
Enabling Always Encrypted is a bit of a process, and one which is made much easier using the Always Encrypted Wizard in SQL Server Management Studio. As you’ll see, the wizard really streamlines the whole process, and makes it virtually painless to take an existing, unencrypted table, and convert it into a table with always encrypted columns in it.
Always Encrypted supports two types of encryption: randomized and deterministic.
Randomized encryption is the most secure, because it randomly and unpredictably generates completely different results for the same value. However, this randomness means that there’s no equality support, because the same data always has a different encrypted value. So you’ll never be able to query on a column using randomized encryption, nor would you be able to join tables on this column, or group, or index, this column, or perform any other operation that requires a deterministic equality test.
For that, you’ll use deterministic encryption, which always generates the same encrypted result for the same data, repeatedly and predictably. This gives you the equality support you need to query on it, index on it, and so on, but is less secure because a hacker could start analyzing patterns to figure out how to decrypt your data. The risk increases significantly when working with small sets of values, particularly Boolean (bit) values like True/False, or Male/Female, because the hacker will only see two distinct values in a bit column that uses deterministic encryption. However, you should be aware that range searching will not be possible even with deterministic encryption, because it’s impossible to determine sequence of values, even if the encrypted value itself is deterministic.
Always Encrypted uses a key to encrypt one or more columns in a table; and thus, we call this key the column encryption key, or CEK. The CEK is used on the client to encrypt and decrypt values in whichever column – or columns – you apply the CEK to. So if you have multiple columns you want to encrypt, you could either create a single CEK and apply it to all the columns, or you could create one CEK for each column, or you could mix it up, and apply one or more CEKs to one or more columns in any combination you’d like. Remember that the CEK is used on the client – it is never exposed in the database, which is what keeps it undecryptable (if that’s a word) at the database level. However, the database does store encrypted versions of the CEKs.
So the columns get encrypted by the CEKs, and the CEKs themselves get encrypted by the CMK, or column master key. And only the client possesses the CMK, so the database can’t do anything with the encrypted CEKs other than give them to the client, who can then use the CMK to decrypt the CEKs, and then use the decrypted CEKs to encrypt and decrypt data.
Always Encrypted requires that the client-side CMK be stored in a trusted location, and supports several options for this, including Azure Key Vault – which is a secure, cloud-based repository, local certificate stores, and hardware security modules. We’ll be using the local certificate store to contain our CMK in the following walkthrough.
Always Encrypted Workflow
Let’s start with a simple Customer table with a few columns that we’d like to protect with Always Encrypted:
Here you see the table with three columns, and we’d like to encrypt the Name and social security number columns. In SQL Server Management Studio, the Always Encrypted wizard creates a column encryption key (CEK) for us, which will be used to encrypt and decrypt the Name and SSN columns on the client (again, multiple CEKs are supported, but not with the wizard). It also creates a column master key (CMK).
Remember that the CMK must be stored in a trusted location, and the wizard supports either the local certificate store or Azure Key Vault.
Once the CMK is stored away (client-side) the wizard uses it to encrypt the CEK, and then stores the encrypted CEK in the database. To be clear, the database receives only the encrypted CEK, which it can’t do anything with given the fact that it doesn’t also receive the CMK. Instead, it receives the client-side location of the CMK. Thus, the database has all the metadata that the client needs for encryption. The client can therefore connect to the database and discover all the CEKs, as well as the path to where the CMK is stored in either the certificate store or Azure Key Vault:
Next, the wizard begins the encryption process, which is to say that it creates a new temporary table, transfers data into it from the Customer table, and encrypts the data on the fly. Again, the encryption occurs client-side, within SSMS, and when it’s finished, the wizard drops the Customer table, and renames the temporary table to Customer:
At this point, we’re done with the wizard and with SSMS. And this is what we’re left with: a database with encrypted data that is absolutely powerless to decrypt itself. Because you need the CEK to decrypt, but the database only has an encrypted CEK. And you need the CMK to decrypt the CEK, but the database only has a path to the CMK (accessible just to the client), not the CMK itself:
So now it’s time to work with this data from your client application. Say you need to retrieve a customer name by querying on the customer’s social security number. Here’s your query, which references the Name column in the SELECT, and the SSN column in the WHERE clause, and both of these columns are encrypted in the database:
Your application passes this query on to ADO.NET, which would normally just pass it on as-is to SQL Server. But that won’t work now because we need to encrypt the social security number first, on the client, before issuing the query. This is going to involve a bit of a conversation between SQL Server and the client, and this conversation gets initiated by including a special setting in the database connection string – Column Encryption Setting=Enabled. With this setting in place, SQL Server will send the encrypted CEK back to the client, and will also send the path to the CMK back to the client. Then the client retrieves the CMK from the trusted location (the local certificate store, or Azure Key Vault) and uses it to decrypt the CEK.
Now the client has a CEK that it can use for encryption and decryption. So it can translate the query and send it out over the wire looking like this:
That is how the query appears “in-flight”, and anyone hacking the wire will only get this encrypted view of the social security number (called the cipher-text). This can work only if we are using deterministic encryption for the social security number, so the same social security number will always encrypt to the same cipher-text value, making it possible to query on it.
What about the Name column? Well, the encrypted name is being returned here in the query result, but we could be using either randomized or deterministic encryption for this column. In this scenario, we can say that we only query on the social security number, but never on the name, in which case we’d prefer to use randomized encryption for the name, which is less predictable and thus more secure than deterministic encryption.
The cipher-text in the query result, whether it’s been randomly or deterministically encrypted, is what gets sent back to ADO.NET on the client, so that on the return trip back, the name in the result is still encrypted. Only when the result arrives at the client does ADO.NET use the CEK to decrypt it, like so:
Encrypting an Existing Table
Let’s see all this work on a real table. I’ll create the Customer table with a few columns, and then populate the table with a couple of rows.
You can see that nothing is encrypted yet, and all the data is clearly visible:
I’m also able to query on the Name column, which is case-insensitive:
I can also run a case-insensitive query on the SSN column.
And of course, I can also run a range query on the SSN column to get all those customers with SSN numbers that start with 5 or greater, and alphabetically, this includes the string n/a.
So now let’s use the wizard to encrypt the name and SSN column with Always Encrypted. I’ll right-click on the database, and choose Tasks > Encrypt Columns… to fire up the Always Encrypted wizard.
Click Next on the introduction page, and now we can select columns for encryption.
First, we’ll encrypt the Name column, and we’ll choose Randomized encryption. We have no requirements to ever query on the name, so we’ll want to choose the more secure encryption type. We’ll also let the wizard create a new column encryption key for the Name column
We’ll also encrypt the SSN column, but this time we’ll choose Deterministic, because we want to be able to query on a customer’s social security number. So it’s necessary to ensure that a given social security number always encrypts to the same cipher-text.
Now what about those warning symbols that appear under State? When I hover, the wizard explains that the collation for these columns is being changed from the normal, case-insensitive (CI) collation, to a binary collation (BIN2), which is case-sensitive. This impacts sorting and querying against the encrypted column, as you’ll see shortly.
Now I’ll click Next, and we can configure the column master key. As with the column encryption key, we’ll let the wizard create a new CMK, and save it to the local certificate store. When you use the local certificate store, it means that the CMK must be stored in a certificate on the local machine.
We have only two rows in our table, so this operation is going to be very quick. But large tables with many rows may take some time to process, and you might not be ready to run that process at the same time that you are running the wizard. Remember that this process involves copying the data from the unencrypted table to a new, temporary table – encrypting the data on-the-fly – and then swapping out the old table for the new one. You don’t want to interrupt this process while it’s running, so the next wizard page gives you the option of running the process now, or saving a PowerShell script that you can run later, at a time that’s more convenient.
We’ll allow the process to run right now, so I’ll click Next once more to reach the Summary page
Here, a quick review shows that the wizard will create a new column master key (CMK) that will be used to encrypt a new column encryption key (CEK), and that the new CMK will be saved to the local machine certificate store. We can also see that the CEK will be used to encrypt the Name and SSN columns, with the Name column using randomized encryption, and the SSN column using deterministic encryption.
Finally, I’ll click Finish to let it run.
In the Certificates console, you can see that a new Always Encrypted certificate was created automatically. This is, in essence, our CMK. If I drill into the certificate details, you can see that the hashing algorithm used by the CMK is SHA256.
Back in SSMS, let’s see how our table has been altered by the actions of the wizard. I’ll right-click the Customer table, and script it out to a new query window:
The CustomerId and City columns look exactly as we defined them, but there’s some new syntax here on the Name and SSN columns. Their datatypes haven’t changed – they are still varchar strings – but the collation has been changed to binary, and ENCRYPTED WITH specifies the CEKs used to encrypt these columns, the encryption type (randomized or deterministic) the encryption algorithm itself.
Now let’s query the table, and, as you might have expected, we only see cipher-text for the Name and SSN columns, because they are encrypted.
Furthermore, we can’t run any queries against either of the encrypted columns.
Now let’s query the system catalog views to discover how our database is using Always Encrypted.
First, we see a master key reference, indicating that the CMK can be found in a certificate store via a local machine path. We also see a column encryption key reference, and the value of that CEK, which is encrypted by the CMK. Again, the unencrypted CEK – which is needed to encrypt and decrypt – is not contained anywhere in the database.
If we query on sys.columns joined with sys.column_encryption_keys, we can see the new metadata columns indicating that the Name and SSN columns are both protected by CEK_Auto1, and that Name is using randomized encryption, while SSN is using deterministic encryption:
Querying and Storing Encrypted Data
So how can we decrypt this data and read it?
Remember, it’s the client that does this, and the client will decrypt it for us if we just connect to the database using the special setting Column Encryption Setting=Enabled.
In this case, the client is SSMS, so just right-click in the query window and choose Connection > Change Connection. Then, under Additional Connection Parameters, plug in column encryption setting=enabled:
This action, of course, resets our connection, so I’ll first switch from master back to our encrypted database, and then query the Customer table once more. And now – like magic – we see the data fully decrypted:
Here’s what’s going on. When we reconnected with the special column encryption setting, that initiated a conversation between the client (SSMS in this case) and SQL Server. SQL Server has passed the CEK (encrypted by the CMK) to the client, as well as the location of the CMK, which is our local certificate store in this case. SSMS has then gone ahead and retrieved the CMK from the certificate store, and used it to decrypt the CEK. At that point, SQL Server returned encrypted data as before, but this time the client (again, SSMS) used the CEK to decrypt the results.
However, in SSMS we are still unable to query on the encrypted columns and we also cannot insert new data for the encrypted columns.
This is because these actions need to be parameterized by an ADO.NET client. To demonstrate, we’ll execute some ADO.NET client code. In Visual Studio, I’ll run some code where I’ve appended column encryption setting=enabled to our connection string, and then I’ll issue the same simple SELECT statement against the Customer. And we can clearly see the name and SSN columns, which have been decrypted.
What about querying directly on the encrypted columns? Well, this should be possible, but with restrictions.
First, because we’re using randomized encryption for the Name column, you can simply never query on it. This is a limitation that we accept in return for the more secure type of encryption, which produces unpredictable values.
The SSN column uses deterministic encryption, making it possible to query against it, but only as a case-sensitive equality query. So when we query for the number of customers where the SSN is n/a, we get a count of 1 for the 1 customer SSN that matches. However, the lowercase n/a SSN will not get matched if we query for N/A in uppercase.
And we’re also restricted to an equality compare; so greater-than, less-than, and BETWEEN queries are not permitted. This makes sense of course, because even though the encryption is deterministic, which produces consistent and predictable results, those encrypted results do not retain the sequence of their unencrypted values.
Now let’s try the INSERT again. This failed in SSMS, but it should work this time, because we’ve set up a parameterized SqlCommand object. So I’ll prepare the command object with the INSERT statement, and then populate the parameters collection with the values for the new name, SSN, and City – where we expect the client to encrypt the SSN and City columns before sending out the command.
Sure enough, we don’t fail this time, and the new customer row gets added successfully:
Returning back to SSMS, querying the table now shows the newly inserted row, along with the earlier three rows, and the data appears decrypted because we’re still running on the SSMS connection with the column encryption setting:
Limitations and Considerations
Before concluding, it’s worth taking a moment to think about some issues you may run into using Always Encrypted. This is a great new technology, but it does have some limitations
First, there is no support for any of the large object and other specialized data types, which cannot be encrypted. There are also other notable incompatibilities with Always Encrypted to point out. Most of these aren’t too onerous, but notably you can’t use Always Encrypted with the new Temporal or Stretch features in SQL Server 2016.
Entity Framework was certainly not designed to work with Always Encrypted, but it is possible. If you’re building applications with EF6 against Always Encrypted tables, then check out this article here which talks you through the various workarounds needed for these two technologies to get along:
You should also always remember that encryption and decryption always happen on the client, and this translates into additional management to ensure that clients can access the CMK. If you’re using the local machine certificate store, this means the additional administrative burden of deploying certificates to all the clients that need access to encrypted data.
And, there are several other restrictions, for which Aaron Bertrand has done an excellent job articulating in his blog post here, at this link:
This post began by reviewing encryption capabilities available in earlier versions of SQL Server, which store keys and certificates on the server, and which encrypts data only at rest
Then I introduced Always Encrypted, a new SQL Server 2016 feature in which the client – not the server – handles encryption and decryption. This makes it a very attractive feature for those customer’s that remain reticent to move their database to the cloud, because they don’t want to move their encryption keys to the cloud as well.
We discussed the two encryption types, where randomized provides more secure encryption based on unpredictable values. Such unpredictability means that you can never query on the encrypted data, and that’s why we also have deterministic encryption. This produces the same encrypted values every time, making it more predictable and les secure, but this also makes it possible to query on the encrypted data, with some restrictions. You learned how the CMK is used to encrypt each CEK, which in turn is used to encrypt the actual data in your tables.