A walkthrough on how to implement encryption on application level using OutSystems Developer Cloud default capabilities.
What is Application Level Encryption?
Application Level Encryption refers to encrypting data within an application, while at-rest encryption is done at a lower level of the technology stack, such as by the Database Management System.
Application Level Encryption is often used - and required - in sensitive environments, where there is a high demand for data protection. This includes protecting data even from otherwise trustworthy internal roles (e.g., ensuring that a database administrator does not have access to sensitive data).
Encryption Types
Encrypting data can be achieved by:
Symmetric Encryption - Also known as shared-key encryption. It encrypts and decrypts data using a single key. Symmetric encryption is widely used for bulk encryption operations because it is computationally cheap.
Asymmetric Encryption - In asymmetric encryption, data is encrypted with one key and can only be decrypted with another key. The encrypting key is called the public key, which is derived from its corresponding private key that can decrypt the data. Asymmetric encryption is computationally expensive and relatively slow. Therefore, it is not commonly used to encrypt data directly but is often used for encrypting keys for symmetric encryption or similar purposes.
In OutSystems Developer Cloud, AES (Advanced Encryption Standard) is available by default for symmetric encryption, and RSA (Rivest-Shamir-Adleman) is available for asymmetric encryption.
Envelope Encryption - DEK and KEK
If you search online for data encryption techniques, you will quickly find articles mentioning Envelope encryption, which is considered a best practice for encrypting large amounts of data.
In Envelope encryption, you create a Data Encryption Key (DEK) to encrypt your data and then use a Key Encryption Key (KEK) to encrypt the DEK. After that, you store the encrypted DEK alongside your encrypted data. A Data Encryption Key is generated for each individual data item and is not reused across multiple data items.
For decryption, you decrypt the encrypted DEK with the KEK, and then decrypt the data with the decrypted DEK.
But why is this considered a best practice? Why not just create one key and encrypt the data directly with that single key? The reason is that if you need to change your encryption key, you would have to decrypt all your data with the old key and then re-encrypt it with the new key. This process can take a very long time. However, with the Envelope Encryption technique, you only need to re-encrypt the Data Encryption Keys. Since keys are usually much smaller than the data, this process is much faster.
Following the envelope encryption approach, you have one critical item: the Key Encryption Key (KEK). With this key, all encrypted Data Encryption Keys and, therefore, all of your data can be unlocked. OutSystems Developer Cloud allows you to store such critical items in Secure Settings.
Important to Consider
In this article, I am showing a pattern for application-level encryption using OutSystems Developer Cloud's default capabilities. Depending on the type of application you want to build, the amount and size of encrypted data, and additional requirements, using the default capabilities alone may not be the best option for you.
In high-volume encryption scenarios, you may quickly face additional challenges not covered in this article, such as scheduled or telemetry-triggered key rotation or multi-wrapping of Data Encryption Keys.
In addition storing the Key Encryption Key in the same environment (OutSystems) as the application poses an additional risk, even though it is stored as a Secret Setting.
Personally, I have had very good experiences with HashiCorp Vault for Key Management and Encryption as a Service, and more recently with AWS Key Management Service and the AWS Encryption SDK as External Logic. I might write an article on the latter.
Implementation
Let us conclude the theory part and look on how to implement this in OutSystems Developer Cloud.
In this pattern we will use AES symmetric keys to encrypt data (Data Encryption Key) and an RSA asymmetric key for encrypting Data Encryption Keys (Key Encryption Key).
To start we first have to create a Setting in our application (e.g. KeyEncryptionKey) set as a secret.
After publishing, we can configure this application setting in the ODC Portal. However, before doing so, we need to create our RSA key that will act as the Key Encryption Key. We will create the key in PEM format.
You can create a PEM file with an RSA private key by using the following PowerShell script:
$rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider(2048)
$rsa.ExportRSAPrivateKeyPem() | Out-File -FilePath 'private-kek.pem'
or by using openssl
openssl genrsa -out private-kek.pem 2048
Open the private-kek.pem file in a text editor and copy the whole content to the clipboard.
In ODC Portal click on the Envelope Encryption Demo, then on Configuration - Settings and copy your key value to the KeyEncryptionKey setting.
Data Encryption and Decryption
With our Key Encryption Key set we are now building two server actions. One for encrypting data and one for decrypting data.
The EncryptData action performs the following steps
It takes a Data input parameter containing any text value that should be encrypted.
Generate a new AES Data Encryption Key with a key length of 256 bits - using AES_NewKey (Security)
Encrypt the input data text using the generated key - using AES_Encrypt found in Security Source.
Converts the Data Encryption Key to a Base64 representation - using BinaryToBase64 (BinaryData). This step is necessary because AES_NewKey returns the key in BinaryData format. However only text can be encrypted using OutSystems Developer Cloud defaults and therefor we convert the key to a Base64 representation.
Reads the RSA Key Encryption Key from Settings - using RSA_KeyFromPEM (Security)
Encrypts the Base64 representation of the Data Encryption Key - using RSA_Encrypt (Security)
Returns the encrypted data and the encrypted Data Encryption Key as BinaryData.
The DecryptData action will perform the following steps
It takes two input parameters EncryptedData containing the encrypted data in BinaryData format and EncryptionKey containing the encrypted Data Encryption Key.
Reads the RSA Key Encryption Key from Settings - using RSA_KeyFromPEM (Security)
Decrypts the encrypted Data Encryption Key - using RSA_Decrypt (Security)
Converts the Base64 representation of the Data Encryption Key to BinaryData - using Base64ToBinary (BinaryData)
Decrypts the data - using AES_Decrypt (Security)
Returns the decrypted Data
Entity Model
With our encryption and decryption actions complete, we can now look at how to store the various pieces of information.
We create the following entities
Report - This entity stores all non-encrypted values of our information. In this simple demonstration we only store
Name (Text) - Name of the report
CreatedOn (Date)- Date when the report was created
DataEncryptionKey - This entity stores the encrypted Data Encryption Keys that were used to encrypt data.
- KeyData (BinaryData) - The encrypted Data Encryption Key
ReportContent - This extension entity stores the encrypted content of the report
ReportId (Report Identifier) - Unique Identifier of the associated Report
Content (BinaryData) - The encrypted data
DataEncryptionKeyId (DataEncryption Identifier) - A mandatory reference to the Data Encryption Key that was used to encrypt this data.
Entity CRUD Wrappers
Having the data model, we create some entity wrapping CRUD actions. For this tutorial, we take a shortcut and implement a save action that handles both creating and updating, as well as another action to get a report.
Save Report
For saving a report (both new and update) we first create a structure Report_Save with the following attributes
Name - Text with a length constraint equivalent to the entity attribute length.
Content - Text without a length constraint.
Then we can create the server action Report_Save with the following input parameters
ReportId (Report Identifier) - Non mandatory. Meaning that if it is a NullIdentifier we consider it to be a new record.
Source (Report_Save Structure) - Mandatory.
Let us walk through this lengthy server action
- EncryptData - We use our created EncryptData server action to encrypt the Content value. Note that this always creates a new Data Encryption Key.
Then we check the ReportId input parameter. If it is a NullIdentifier(), we consider it a new report; otherwise, it is an existing report.
New Record
CreateDataEncryptionKey - Creates a new Data Encryption Key record for the generated and encrypted Data Encryption Key of the EncryptData action.
CreateReport - Creates the Report record.
CreateReportContent - Saves the encrypted content along with a reference to the created Data Encryption Key.
Existing Record
GetReportForUpdate - This retrieves the Report record for the given Report Identifier. Using this entity action has two benefits. First it throws a database exception error if the record is not found and second it locks this Report record and therefore prevents simultaneous updates.
GetReportContent - This aggregate gets the associated ReportContent record joined with the associated Data Encryption Key Record
Report.Name - Sets the Name attribute of the GetReportForUpdate record.
DataEncryptionKey.KeyData - Sets the KeyData attribute of the GetReportContent result to the new generated and encrypted Data Encryption Key of the EncryptData action.
UpdateDataEncryptionKey - Updates the Data Encryption Key record
ReportContent.Content - Sets the Content attribute of the GetReportContent result to the encrypted data of the EncryptData action.
UpdateReportContent - Updates the ReportContent record.
UpdateReport - Updates the Report record and releases the lock.
You might wonder if it's really necessary to always create a new Data Encryption Key when updating a record, instead of just using the existing one. The answer is no, but doing it this way gives us an easy key rotation.
Get Report
The Report_Get action retrieves a report including its content unencrypted. We start by defining a structure Report_Result with the following attributes
Name - Text.
Content - Text.
CreatedOn - Date when this report was created.
Report_Get takes a mandatory input parameter ReportId (Report Identifier) and returns a Result output property (Report_Result Structure).
GetReportById - This aggreate joins Report, ReportContent and DataEncryptionKey and returns a single result filtered to ReportId.
If there is no record we just exit out here.
DecryptData - Decrypts the content using the stored Data Encryption Key.
Name, Content, CreatedOn - Returns the result.
Frontend
The UI layer of the demo application should be pretty self-explaining, so I will not go into details here.
The Reports screen queries the Report entity and displays a table with all existing reports. To edit a Report you click on the Name and to create a new Report you click on the Create a Report button in the action section.
The Report screen uses a data action to get the details of a report using the Report_Get action and if you click the Save button the Report_Save action is executed.
Summary
OutSystems Developer Cloud brings everything to implement simple application level encryption by applying envelope encryption using an asymmetric Key Encryption Key and symmetric Data Encryption Keys. However in more demanding and critical scenarios you should aim for an integration with at least an external Key Management Solution or even with an external Encryption as a Service Solution.
Thank you for reading. I hope you enjoyed it and that I've explained the important parts clearly. If not, please let me know ๐ Your feedback is greatly appreciated.
Follow me on LinkedIn to receive notifications whenever I publish something new.