Using the most basic setup:
class User < ActiveRecord::Base attr_encrypted :name, key: 'This is a key that is 256 bits!!', encode: true, encode_iv: true, encode_salt: true end
The results look like this in the database when supplying an identical name:
╔════╦══════════════════════════════╦═══════════════════╗ ║ id ║ encrypted_name ║ encrypted_name_iv ║ ╠════╬══════════════════════════════╬═══════════════════╣ ║ 1 ║ aVXZb1b317nroumXVBdV9pGxA2o= ║ JyE7wHups+3upY5e ║ ║ 2 ║ aVXZb1b317nroumXVBdV9pGxA2o= ║ uz/ktrtbUAksg5Vp ║ ╚════╩══════════════════════════════╩═══════════════════╝
Why is the ciphertext identical? Isn't that the part of the point of iv, which the gem is using by default?
2 Answers
Answers 1
Update: the following is the original post explaining the whole problem, the issue is fixed now, see the bottom of this answer for a solution.
I am quite sure you noticed a rather nasty security issue in the encryptor
gem (the gem that is used by attr_encrypted
to do the actual encryptions).
The problem is that when using the aes-256-gcm
algorithm (or any of the AES GCM
algorithms), the initialization vector (IV) is currently indeed not taken into account when encrypting. The issue does not affect other algorithms but unfortunately the aes-256-gcm
is the default algorithm in attr_encrypted
.
As it turns out, it is the order of setting the IV vs. the encryption key what causes the issue. When IV is set before the key (as is in the gem), the IV is not taken into account but it is if set after the key.
Some tests to prove the problem:
While taking parts of the encryptor
gem code, I created the simplest test case to prove the problem (tested under ruby 2.3.0 compiled against OpenSSL version "1.0.1f 6 Jan 2014"):
def base64_enc(bytes) [bytes].pack("m") end def test_aes_encr(n, cipher, data, key, iv, iv_before_key = true) cipher = OpenSSL::Cipher.new(cipher) cipher.encrypt # THIS IS THE KEY PART OF THE ISSUE if iv_before_key # this is how it's currently present in the encryptor gem code cipher.iv = iv cipher.key = key else # this is the version that actually works cipher.key = key cipher.iv = iv end if cipher.name.downcase.end_with?("gcm") cipher.auth_data = "" end result = cipher.update(data) result << cipher.final puts "#{n} #{cipher.name}, iv #{iv_before_key ? "BEFORE" : "AFTER "} key: " + "iv=#{iv}, result=#{base64_enc(result)}" end def test_encryption data = "something private" key = "This is a key that is 256 bits!!" # control tests using AES-256-CBC test_aes_encr(1, "aes-256-cbc", data, key, "aaaabbbbccccdddd", true) test_aes_encr(2, "aes-256-cbc", data, key, "eeeeffffgggghhhh", true) test_aes_encr(3, "aes-256-cbc", data, key, "aaaabbbbccccdddd", false) test_aes_encr(4, "aes-256-cbc", data, key, "eeeeffffgggghhhh", false) # failing tests using AES-256-GCM test_aes_encr(5, "aes-256-gcm", data, key, "aaaabbbbcccc", true) test_aes_encr(6, "aes-256-gcm", data, key, "eeeeffffgggg", true) test_aes_encr(7, "aes-256-gcm", data, key, "aaaabbbbcccc", false) test_aes_encr(8, "aes-256-gcm", data, key, "eeeeffffgggg", false) end
Running test_encryption
which encrypts a text using AES-256-CBC
and then using AES-256-GCM
, each time with two different IVs in two regimes (IV set before/after key), gets us the following results:
# control tests with CBC: 1 AES-256-CBC, iv BEFORE key: iv=aaaabbbbccccdddd, result=4IAGcazRmEUIRDE3ZpEgoS0Nmm1/+nrd5VT2/Xab0WM= 2 AES-256-CBC, iv BEFORE key: iv=eeeeffffgggghhhh, result=T7um2Wgb2vw1r4uryF3xnBeq+KozuetjKGItfNKurGE= 3 AES-256-CBC, iv AFTER key: iv=aaaabbbbccccdddd, result=4IAGcazRmEUIRDE3ZpEgoS0Nmm1/+nrd5VT2/Xab0WM= 4 AES-256-CBC, iv AFTER key: iv=eeeeffffgggghhhh, result=T7um2Wgb2vw1r4uryF3xnBeq+KozuetjKGItfNKurGE= # the problematic tests with GCM: 5 id-aes256-GCM, iv BEFORE key: iv=aaaabbbbcccc, result=Tl/HfkWpwoByeYRz6Mz4yIo= 6 id-aes256-GCM, iv BEFORE key: iv=eeeeffffgggg, result=Tl/HfkWpwoByeYRz6Mz4yIo= 7 id-aes256-GCM, iv AFTER key: iv=aaaabbbbcccc, result=+4Iyn7RSDKimTQi0S3gn58E= 8 id-aes256-GCM, iv AFTER key: iv=eeeeffffgggg, result=3m9uEDyb9eh1RD3CuOCmc50=
These tests show that while the order of setting IV vs. key is not relevant for CBC, it is for GCM. More importantly, the encrypted result in CBC is different for two different IVs, whereas it is not for GCM if IV set before the key.
I just created a pull request to fix this issue in the encryptor
gem. Practically, you have a few options now:
Wait till a new version of the
encryptor
gem is released.Use also salt with
attr_encrypted
. You should use salt anyway to further secure the encrypted data.
The very unfortunate thing is that all already encrypted data will become undecipherable after the fix as suddenly the IVs will be taken into account.
Update: encryptor
3.0.0 available
You can now upgrade the encryptor
gem to version 3.0 in which the bug is fixed. Now, if this is the first time you use the encryptor
or attr_encrypted
gems you are all set and everything should work correctly.
If you have data that is already encrypted using encryptor
2.0.0, then you must manually re-encrypt the data after the gem upgrade, otherwise it will fail to decrypt correctly! You will be warned about this during the gem upgrade. The schematic procedure is as follows:
- You have to decrypt all your encrypted data using the
Encryptor
class (see the README for examples), using the:v2_gcm_iv => true
option. This should correctly decrypt your data. - Then you must encrypt the same data back again, now without this option (i.e.
:v2_gcm_iv => false
) but including the proper IV from your database. - If you have production data, you will need to do this upgrade offline and immediately after the gem update to ensure no data corruption.
Answers 2
If the plain text, key and IV are the same the encrypted text will be the same.
It looks like you need to use:
attr_encrypted :email, key: 'some secret key', encode: true, encode_iv: true, encode_salt: true
Note: encode_iv: true
Or perhaps set the default option encode_iv: true
Documentation: attr_encrypted
0 comments:
Post a Comment