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.
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 -----BEGIN RSA PRIVATE KEY----- MIIEpgIBAAKCAQEAwuz4n8Q2v1d7+t0yQAxSyFlzMSyyl+eAk3hnZDTa3Sbj6PZN /xXjrm9OWmetWJzz31OJjc4DwjC/97BLBhEf3ukUuig6+hDAQWIMARjWqEo/1yLZ ZM2wiZ25JNYAf3KgUlmR5Ks4WcGiDFobhBVIZPVTt1/6ELvyOS6kY94WiJHLtG1F cRDoPt//we9z0ocGizGvQL6WEMu/VvTrbwr8xYinMpW3osc7oSsTal+Utu0cz0tz AxSqEm4y174CFvHJGEpgAh/ljqFuV44Cl5PC8rqDJg29cSpIJp5S7hKaZbflOq+J tD7BNdo+ZmIiGLP+bWpXPhhw+hBglpK7Hqo7tQIDAQABAoIBAQCwdtt1t6pgepCw wQM23HEtE12nTPG5d0j9OGlRXFAvGYAGbMSbg3OFfRqP2YAi0qQsr3G9wJ3CdWO0 lhK1QVd688Nh6/3IWNXT2zFG5PefjuhQmSn5igSh8PmlkV8OAfWF17SuMRtollVf nUt/vcy2KSpKvkaiU6OrhMAp8OqxYsdWhwtaJNjJQYKiAERrbP1PnuB8ftwWnLAr g4ShKDb1Lys3/nFhyLFg7qSmXs4xAZA6aUvtjhIizeQuXd0UR12Arsco/vrehGRe U0y5R07bshNy4LTSTG3IOH2DU1MrcTw5uhylE22EEKfsAY7O2JpBjxioE++fga4/ sZAefQHBAoGBANo4wzGy36g5XnsxxmYzSV/PwDxmAqSBfHaMBdEH1Vh3jr/YMfrf XMe6IGWVW+jLbUE3YSsm1HUh78CWmuZgYMEh+Fx0xSzYr2tL3p+MX2hDOUL05hJL mTjbn/FLPDRTrQ/5+O1o4zBTI7pydhvYH90vYi20thWeUbyB44aZ7NSpAoGBAOSr w8BBDWGMJSwB3IO88CuyKlkdYCE05tGHh11rIeWFld3UWsYXUD1E/j7VIP4rnoFR eRVfeVhpj1Jd2ROTIY246QRbD1VDpe/x7Uh/Dt3B+h87J/aXhDN94b/LpwCU+Gj4 yIhl1VHfrK+vbighdmVSQQonbQquhhU8cTXQEEotAoGBAM7FTh7/UHFDusSci1M3 cWT5ozsXpZVepCJn1vMTqxGiZ35cSi9eCbmuIRhgB7BzYNiUstuCdXlvaI9hpPB5 jfQyTfS9KD+wKbdPMmiXR6exWsaY6o+XVl3LrKekFC24w5kJ0NaTtgGKJaZ64nLL vJWGWk7Yllexpd0qbf6SRxfRAoGBAJZE1cdyOFPhH9BSjNG5iG5+j1uudSx9Mi2B DZBzRXwqE/kJgnloep84xocN0beVfHzoyFQmQHy8KaXr7Cnz5vnWCLKHEIVshhAv AEpCzMcnoLGDU1i16vdXgtFiCCXWv4Nj8YvIt60s+rMc6pvOmZotunXswLhjRdOQ u6isSPglAoGBALSeGHQ1Fkf29rMG1VxsH0mXEGLhNEGFRTglwS0LxkCS76xnuDZO 7YzU0HuPfc3n7d66Di67Haut3itWmrbP1HVDlhLeon55Hi1RKPmn9prr8XGyXO2f 9UGq1l/vqE5lJvu57s55tBrAKPt4hiQHyQd5PQ+LxFboZ1kr09G5Kfe8 -----END RSA PRIVATE KEY----- >>> print alice_pubkey -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwuz4n8Q2v1d7+t0yQAxS yFlzMSyyl+eAk3hnZDTa3Sbj6PZN/xXjrm9OWmetWJzz31OJjc4DwjC/97BLBhEf 3ukUuig6+hDAQWIMARjWqEo/1yLZZM2wiZ25JNYAf3KgUlmR5Ks4WcGiDFobhBVI ZPVTt1/6ELvyOS6kY94WiJHLtG1FcRDoPt//we9z0ocGizGvQL6WEMu/VvTrbwr8 xYinMpW3osc7oSsTal+Utu0cz0tzAxSqEm4y174CFvHJGEpgAh/ljqFuV44Cl5PC 8rqDJg29cSpIJp5S7hKaZbflOq+JtD7BNdo+ZmIiGLP+bWpXPhhw+hBglpK7Hqo7 tQIDAQAB -----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) '\xbbx\x97\xc8\n.a0\xd4\xba\x07\xec\xc8\x83wK\xc9N8\xef\xe2\x1dVr\xc5\xfe\xf0\x05R+;\xd2' >>> privkey = RSA.importKey(bob_privkey) >>> privkey.decrypt(bob_masterkey) '\xbbx\x97\xc8\n.a0\xd4\xba\x07\xec\xc8\x83wK\xc9N8\xef\xe2\x1dVr\xc5\xfe\xf0\x05R+;\xd2'
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) '\xbbx\x97\xc8\n.a0\xd4\xba\x07\xec\xc8\x83wK\xc9N8\xef\xe2\x1dVr\xc5\xfe\xf0\x05R+;\xd2'
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
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.