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?
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.
encryptor
3.0.0 availableYou 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:
Encryptor
class (see the README for examples), using the :v2_gcm_iv => true
option. This should correctly decrypt your data.:v2_gcm_iv => false
) but including the proper IV from your database.openssl
gem confirmed and fixedFYI, it was recently confirmed that this had actually been an issue in the underlying ruby-openssl library and the bug has been fixed now. So, in the future, it is possible that even attr_encrypted
gem version 2.x will actually work correctly when used with the new openssl-2.0.0
gem version (which is now in beta as of Sep 2016).