Looking for a TL;DR?
In this scenario we have three nodes to our process:
- The Angular application (Client)
- Processing Server (Server)
- API Endpoint (Endpoint)
If encryption took place on the server the data would be not encrypted apart from HTTPs between the client and the first server, as per the clients specification our server should not be able to read the data in any way. To get the process to work this way we would have to handle the encryption process on the client.
This opens up the possibility of anyone opening up the application source code and taking a peek at its inner workings and any shared secrets within. So security by obscurity, as ever, is a big no-no here. This would ensure our encryption would be difficult to undo even if you know exactly how it works. The solution use a public key to encrypt the data that uses a private key that only the client has to decrypt it when they need it. We needed a asymmetric encryption approach.
My first thought went straight to the most ubiquitous encryption method I come across in my job, RSA keys. I frequently use these for SSH'ing into servers and creating SSL certificates requests. After a brief search for RSA implementations I came across jsencrypt, a library that performs encryption/decryption and key generation. I generated a key pair on my machine via the command line (start with what you know).
We start by generating a private key. The following command will spit out a
.pem file with the name "rsa_private". The
1024 at the end of the command is the key size. Bigger the key the bigger the message you can encrypt, but it will increase encryption time and may affect application performance.
openssl genrsa -out rsa_private.pem 1024
Once we have our private key we can use it to generate a public key to match using the following command:
openssl rsa -pubout -in rsa_private.pem -out rsa_public.pem
You can read these keys out quickly using
cat on the command line:
Once I had my keys I started to play around with encrypting and decrypting small strings. For example to encrypt:
// Encrypt let message = "Hello world"; const public_key = "-----BEGIN PUBLIC KEY-----MIGfMA0GCSq[...]"; let encrypt = new JSEncrypt(); encrypt.setPublicKey(public_key); let cipher_text = encrypt.encrypt(message);
And again for the decryption:
// Decryption const private_key = "-----BEGIN RSA PRIVATE KEY-----GSIb3DQEBAQUAA4[...]"; const cipher_text = "SUOCILmCAPTyO/8P0itbzm[...]"; let decrypt = new JSEncrypt(); decrypt.setPrivateKey(private_key); let result = decrypt.decrypt(cipher_text); // result = "Hello world"
Turns out RSA is only able to encrypt data to a maximum amount of your key, but as our data will vary in length, as such this did not make RSA a good solution for our JSON data, and incidentally this is not what RSA is designed to do. I quickly discovered what I was after was a block-cypher.
The Advanced Encryption Standard (AES) is a symmetrical encryption algorithm, that uses the same keys for encrypting the plain text as it does for decrypting the cypher text, you can think of these as a shared secret people can use to jointly decrypt the cypher text.
With AES’s operation modes allow it different methods the length of the data you wish to encrypt. AES has a number of operation modes, these change the way the algorithm works and ultimately how difficult the cypher text is to break.
I settled on the Counter (CTR) mode due to the need to keep the initialisation vectors separate for each operation. IMO - this might not be the best approach for what I was trying to do but hey I got it working.
Due to the size of our data payload we can't just use RSA (as I naively thought at the outset). What would work however is a combination of symmetrical encryption and asymmetrical. With my trusty graph editor yEd, I started diagramming an outline of how this would work. See the graph below, outlining my planned approach. Without the private RSA key the transmitted payload is useless.
We start by randomly generating the AES counter and key for each round of encryption. Then we encrypt your payload with these, leaving you with an encrypted payload, and a plain text counter and key. We then use the public RSA key to encrypt the counter and key leaving us with all three components encrypted.
Once we have transmitted our data the client can then decrypt using their private key. First use that key to decrypt the counter and key for the AES encryption. Then pass these to the AES decryption process, leaving us with an unencrypted payload.
TL;DR - RSA keys are too small for large encryption. You can solve this by using AES to encrypt your data and then RSA encrypt the AES components (key and initialisation vectors) to have a fully encrypted payload. Winning!