Have you been looking for a better way to model your network infrastructure? Check out what we're doing with NetBox! Open source and widely extensible, NetBox has enabled thousands of organizations to automate their networks like never before possible.

Can You Keep a Secret? (Part 2)

By stretch | Thursday, February 11, 2016 at 2:36 a.m. UTC

In part one, we saw how AES can be used to encrypt sensitive data so that it can be retrieved only by using an encryption key. The problem with this approach is that everyone who needs access to the data must have a copy of the key. If any one of these copies becomes compromised, the entire database must be re-encrypted using a new key, and the new key must be distributed securely to all parties involved. In this article, we'll see how symmetric encryption can be combined with asymmetric cryptography (namely RSA) to create a hybrid cryptosystem.

Let's begin by encrypting some data using AES as we did in part one. First we pad our plaintext's length to a multiple of 16 using null bytes, then generate a 256-bit encryption key and a 128-bit IV, and finally encrypt it with CFB-mode AES to generate a string of ciphertext.

>>> from Crypto.Cipher import AES
>>> import os
>>> plaintext = "Operation Neptune will launch on June 6th"
>>> plaintext += (16 - len(plaintext) % 16) * chr(0)
>>> encryption_key = os.urandom(32)
>>> iv = os.urandom(16)
>>> cipher = AES.new(encryption_key, AES.MODE_CFB, iv)
>>> ciphertext = cipher.encrypt(plaintext)

The IV generated for each secret will be stored alongside the ciphertext in the database, so that anyone possessing the encryption key can retrieve the encrypted data. (Note: Because we're using CFB mode, the cipher object needs to be re-initialized with the original IV each time we want decrypt the plaintext.)

>>> cipher = AES.new(encryption_key, AES.MODE_CFB, iv)
>>> cipher.decrypt(ciphertext).replace('\x00', '')
'Operation Neptune will launch on June 6th'

Our problem now is what to do with the encryption key. Obviously, we can't store it on the same system as the encrypted data, as that would defeat the entire purpose of the encryption. We could distribute a copy of the encryption key to each of the users who need access to the encrypted data, but that increases the odds of the key becoming compromised.

Asymmetric cryptography is very useful in this situation. Asymmetric cryptography employs keys in pairs: one public key for encryption, and one private key for decryption. The public key can be freely shared with anyone, while the private key is kept secure (and ideally never leaves a trusted system). Only the private key can be used to decrypt encrypted data.

The downside to asymmetric encryption, relative to symmetric encryption like AES, is that it's slow and very limited in the amount of data that it can encrypt. For instance, a 2048-bit RSA key can encrypt up to 245 bytes of data (256 bytes of key material minus 11 bytes of overhead). In contrast, an AES key (of any length) can be used to encrypt any amount of data. Therefore, instead of trying to encrypt all our secret data with RSA directly, we'll stick with our AES implementation and simply encrypt the AES key itself with RSA.

Asymmetric Encryption

Suppose we have two users - Alice and Bob - who each need access to the encrypted data. Our first step is to generate an RSA key pair for each user. (Ideally, each user would generate his or her own key pair and submit only the public key to ensure the private key cannot be intercepted.) We'll use pycrypto's RSA library for this.

>>> from Crypto.PublicKey import RSA
>>> keypair = RSA.generate(2048)
>>> alice_privkey = keypair.exportKey()
>>> alice_pubkey = keypair.publickey().exportKey()
>>> keypair = RSA.generate(2048)
>>> bob_privkey = keypair.exportKey()
>>> bob_pubkey = keypair.publickey().exportKey()

Keys are exported in ASCII-friendly PEM format, which looks like this:

>>> print alice_privkey
>>> print alice_pubkey
-----END PUBLIC KEY-----

Next, we'll generate an encrypted copy of the AES master key for each user.

>>> pubkey = RSA.importKey(alice_pubkey)
>>> alice_masterkey = pubkey.encrypt(encryption_key, None)
>>> pubkey = RSA.importKey(bob_pubkey)
>>> bob_masterkey = pubkey.encrypt(encryption_key, None)

While both copies contain the same information (the AES master key), alice_masterkey can only be decrypted using Alice's private key, and bob_masterkey can only be decrypted using Bob's private key.

>>> privkey = RSA.importKey(alice_privkey)
>>> privkey.decrypt(alice_masterkey)
>>> privkey = RSA.importKey(bob_privkey)
>>> privkey.decrypt(bob_masterkey)

We can now destroy the original encryption key. It's important to understand, though, that the encrypted data can now be retrieved only if we have Alice's or Bob's private key. If both of those keys are lost, so is the entire database.

What if we need to add a third user, Charlie? Alice or Bob will need to decrypt their copy of the master key and re-encrypt it using Charlie's public key.

>>> keypair = RSA.generate(2048)
>>> charlie_privkey = keypair.exportKey()
>>> charlie_pubkey = keypair.publickey().exportKey()
>>> privkey = RSA.importKey(alice_privkey)
>>> encryption_key = privkey.decrypt(alice_masterkey)
>>> pubkey = RSA.importKey(charlie_pubkey)
>>> charlie_masterkey = pubkey.encrypt(encryption_key, None)

Now, Charlie has his own copy of the encryption key, which he can decrypt using his own private key.

>>> privkey = RSA.importKey(charlie_privkey)
>>> privkey.decrypt(charlie_masterkey)

So long as users don't share or lose their private keys, the database will remain secure. And if we need to revoke a user's access to the database, we can simply delete his or her copy of the encryption key.

The code snippets above are obviously for illustration only. In a real system, users would never interact with encrypted data directly, and no user would ever have access to the encryption key.

As for real-world implementation, there are two ways to approach such a system. One option would be to have users download encrypted data and the encrypted master key, and perform all decryption locally. This ensures each user's private key need never leave their local machine. However, it also grants each user access to the decrypted master key. This means that a revoked user can still decrypt data unless the entire database has been re-encrypted with a new key.

A better approach might be to have the user send his private key to the server, perform all decryption remotely, and respond with the plaintext data. This would seem to break asymmetric cryptography rule number one (never share your private key), but in this specific instance we're using RSA to protect data at rest, rather than data in transit. We also use another layer of encryption, in the form of SSL/TLS, to protect our communication between the client and server, just as we would do for any web application. And it means we never have to expose the master key.

Hopefully these little exercises have shed some light on the common types of cryptography and why we use them. Cryptography can be pretty fun when you get to know it.

Posted in Security

Support PacketLife by buying stuff you don't need!


February 24, 2016 at 2:40 p.m. UTC

Great article I do not understand how the following works "And if we need to revoke a user's access to the database, we can simply delete his or her copy of the encryption key."

As long as the user has his encrypted copy of the db encryption key + private key = db encryption key, is there a way to revoke user's access ???

May 18, 2016 at 6:03 p.m. UTC

antony, I had the same question and I think that's what the author meant: In the second case, if db master key is not stored on a User's side and a User gets compromised then db master key may not be compromised with it. I guess the main difference is that an attacker would have to do an extra step :) IMHO this is not a good point in this article. But the article itself is nice.

Comments have closed for this article due to its age.