In the previous article we discussed the challenges that developers encounter while working with cryptography APIs, using an example. We have shown that getting the code running is a non-trivial task and neither is making it secure. FluentCrypto aims to mitigate these problems by providing a wrapper for the Node Crypto API.
Let us revisit the example from the last article. In the end, Matt's code looked like this:
const crypto = require('crypto'); | |
const key = crypto.scryptSync(process.env.PRIVATE_KEY, 'salt', 8); | |
const iv = Buffer.alloc(8,0); | |
const algorithm = 'des'; | |
const cipher = crypto.createCipheriv(algorithm, key, iv); | |
let encrypted = cipher.update('first part that came in', 'utf8', 'hex' ); | |
encrypted += cipher.update('second part that came in' , 'utf8', 'hex'); | |
encrypted += cipher.final('hex'); | |
// Somewhere else, later | |
const decrypter = crypto.createDecipheriv(algorithm, key, iv); | |
let decrypted = decrypter.update(encrypted, 'hex', 'utf8'); | |
decrypted += decrypter.final('utf8'); |
Rewritten with FluentCrypto, the code now looks like this:
const FluentCrypto = require('./FluentCrypto'); | |
const key = FluentCrypto.generateSymmetricKeyFromPasswordFor('cipher', 'des', process.env.PRIVATE_KEY); | |
let encrypter = FluentCrypto.Encryption() | |
.withCipher('des') | |
.key(key) | |
.data('I came in first') | |
.data('and I came in second'); | |
let encrypted = encrypter.encrypt(); | |
//Later, Somewhere | |
let decrypted = FluentCrypto.Decryption() | |
.from(encrypter) | |
.data(encrypted) | |
.decrypt(); |
This code has the same functionality as the original code, but is a lot shorter and more secure!
1- Easy key generation
On line 3 the FluentCrypto module has created a key of the correct length for the chosen algorithm from our environment variable.
2- Automatic creation of the initialization vector
Over the next lines, an encryption object is created (line 5) which is then told to use a cipher with a certain algorithm (line 6) and then provided with the configurations and data in a straightforward way. In the background, an initialization vector of correct length for our chosen algorithm is created. Since you will want to store the initialization vector for long term usage, FluentCrypto provides getters for every configuration as can be seen below.
//retrieve iv from encrypter for storage
const iv = encrypter.getIV();
storeIV(iv);
3 - Simple decryption
To decrypt with FluentCrypto, the encryption object itself can be used. The module creates the decryption object with the matching attributes (line 14). This feature is meant to be used with objects that are still available in memory. Of course, if the encryption object would not be available anymore or if you want to perform the decryption on another machine, then you would want to set the key, algorithm and iv similarly to the encryption object.
const key = FluentCrypto.generateSymmetricKeyFromPasswordFor('cipher', 'des', process.env.PRIVATE_KEY) | |
const iv = retrieveIVFromStorage(); | |
const encrypted = getEncryptedData(); | |
let decrypted = FluentCrypto.Decryption() | |
.fromCipher(‘des’) | |
.iv(iv) | |
.key(key) | |
.data(encrypted) | |
.decrypt(); |
4- Throwing errors on insecure usage
If we run this code, it will result in an exception, but that’s actually a good thing. As mentioned in the last article, the algorithm we chose is not considered secure. Therefore, FluentCrypto will throw the following error: “AlgorithmNotAllowed: des is not allowed here. Use one of aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm, aes-256-cbc, aes-256-gcm”. The error message explains what the problem is and how it can be solved.
After following the instruction, the code looks like this:
const FluentCrypto = require('./FluentCrypto'); | |
const key = FluentCrypto.generateSymmetricKeyFromPasswordFor('cipher', aes-128-cbc, process.env.PRIVATE_KEY) | |
let encrypter = FluentCrypto.Encryption() | |
.withCipher('aes-128-cbc') | |
.key(key) | |
.data('I came in first') | |
.data('and I came in second'); | |
let encrypted = encrypter.encrypt(); | |
//Later, Somewhere | |
let decrypted = FluentCrypto.Decryption() | |
.from(encrypter) | |
.data(encrypted) | |
.decrypt(); |
No more error is thrown. Compared with the Node Crypto API example, we didn't have to do any concatenation or set any encodings to make it work. FluentCrypto did this work for us. It also provided us with some quality of life improvements, such as setting the key and initialization vector on the decryption object from the encryption object. Additionally, we can feel safe that this code is actually considered secure. Otherwise, an error would have been thrown.
How does FluentCrypto work?
FluentCrypto is a wrapper around the Node Crypto API based on the Fluent Interface pattern. This pattern is based on method chaining with methods named so that a chain of method calls reads like normal language. Well known examples for this are jQuery or the visualisation library D3. Using this pattern gives the users a better understanding on what they are doing and what they should do next.
For the structure of the API, small possible cryptographic tasks, such as “Set a key” and “Select an algorithm” were identified. For each of these tasks, an API call was then created that solves the task. FluentCrypto users can identify the calls they want to make from the tasks they want to solve, making it easier to solve cryptographic tasks.
The other main part of FluentCrypto is making sure that what the user wants to do is secure and notifying the user if it is not. A key point is the error message being helpful. An error message should give the user a clear understanding of what went wrong, where it went wrong, why it went wrong and possibly how he could fix it. To achieve this, FluentCrypto uses CryRule files. CryRule is a small, domain-specific language that has been developed for the FluentCrypto project and is basically a rulefile. It states which cryptographic concepts/methods (Hash, Cipher, etc.) can use which algorithms, it can forbid certain algorithms or put further constraints on certain algorithm choices.
CryRule follows a whitelisting approach, meaning that every configuration that is not listed is considered insecure. The CryRule file used for the example earlier looks like this:
Cipher | |
algorithm | |
IN [aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm, aes-256-cbc, aes-256-gcm] | |
symmetricKey : SymmetricKey | |
MANDATORY AFTER publicKey | |
LENGTH 16 IF algorithm IN [aes-128-cbc, aes-128-gcm] | |
LENGTH 24 IF algorithm IN [aes-192-cbc, aes-192-gcm] | |
LENGTH 32 IF algorithm IN [aes-256-cbc, aes-256-gcm] | |
iv | |
LENGTH 16 IF algorithm IN [aes-128-cbc, aes-192-cbc, aes-256-cbc] | |
LENGTH 96 IF algorithm IN [aes-128-gcm, aes-192-gcm, aes-256-gcm] |
In this example, the algorithm choice for ciphers is constrained to the list on line 3. Furthermore, some constraints concerning the iv and key are defined for certain algorithms.
These rule files are read and parsed at runtime by a parser developed with the Chevrotain library. Chevrotain allows one to easily define grammars and parsers for domain-specific languages such as CryRule.
Revisiting our example, Matt wanted to use the algorithm “des”. As can be seen on line 3, this algorithm is not whitelisted, therefore an error is thrown. This error uses the whitelisted list to also suggest the user a different algorithm.
Another usage of the rule file are defaults. Since every listed configuration is considered secure, a default taken from such a file is a secure and sensible choice. Again revisiting our example, we do not provide a configuration for an initialization vector. FluentCrypto then uses a default value taken from the rules, where length 16 (bytes) is listed for our chosen algorithm (aes-128-cbc).
A big advantage of these rule files is that security experts can read and write them without needing any knowledge about JavaScript. Even knowledge about the Node Crypto API itself is rather optional for the experts.
This also relieves the developers and maintainers of a project such as FluentCrypto, since they do not need to worry about their cryptography knowledge.
In conclusion, this prototype allows us to solve the same tasks as the Node Crypto API while providing us with some quality of life improvements, making our code easier to read and understand while also making it more secure.
Limitations and future work
FluentCrypto is only a prototype, a proof of concept. The rule files are currently not written by an expert and the API covers only a small part of the whole Node Crypto API. FluentCrypto also only covers local development and does not support developers in network tasks such as key exchanges in its current state. Future resources should be invested in covering more of the API and making sure that the rule files are written by experts. The CryRule language already satisfies most futures needs of cryptographic rules and is open to future extensions. The same holds for the FluentCrypto API itself. But first, the concept needs to prove itself. Evaluation is currently running (see the next section if you are interested in participating) and has not yet concluded, but the first feedback is promising.
Evaluation
To evaluate the prototype, we are carrying out a survey. During this survey, the developers are asked several questions about their background to test their background knowledge about cryptography and NodeJS. Then they are asked to solve three basic tasks with both the Node Crypto API and the FluentCrypto prototype. Their subjective feedback as well as their written code will then serve to evaluate the usage and usefulness of FluentCrypto. If you are interested in participating, please follow one of the following links:
Survey with File Upload (Google Account required)
Survey without File Upload (No Account required)
Once the evaluation has concluded, a follow-up article will be posted with the result.
Top comments (0)