Closed zeknox closed 7 years ago
This is expected behavior with the AES encryptor. The encryptor's usage of AES.encrypt
generates a different initialization vector (iv) for each encrypt
call, so you see different ciphertext for the same plaintext/key.
>> AES.encrypt("foo", "secret")
"hhOfJ+F2mUgA2h0XpNIUTA==$BHdRvIJQZSubl6gHJ+nhqw=="
>> AES.encrypt("foo", "secret")
"AlpzfBRR2toFoQOlS8G5sQ==$JnNoXcOcWshvcT6xxkst7Q=="
ActiveRecord uses the ciphertext when validating uniqueness, not the plaintext. So the test is actually failing correctly if the expectation is that the plaintext is unique.
If you are trying to validate the ciphertext itself is unique, you can try stubbing AES.encrypt
in your spec so it returns the same value and your test would run as expected.
If you are trying to validate the plaintext is unique, there are a few options you can try.
Use :mysql_aes_new
MySQL's AES_ENCRYPT
function returns the same ciphertext when passed the same plaintext/key. If you are using MySQL for your DB and don't specifically need to use the AES
gem, this is probably the quickest thing to do.
Use a static iv with AES
AES.encrypt
accepts an iv
parameter. If you use the same iv
the ciphertext will be the same.
>> iv = AES.iv(:base_64)
"qswUlsJEkWCcRXClFgz51Q=="
>> AES.encrypt("foo", "key", iv: iv)
"qswUlsJEkWCcRXClFgz51Q==$5ywZjOowOmnt5CXXWwzsxg=="
>> AES.encrypt("foo", "key", iv: iv)
"qswUlsJEkWCcRXClFgz51Q==$5ywZjOowOmnt5CXXWwzsxg=="
To enable this in CryptKeeper you can create your own encryptor or just monkey patch:
# Probably want to store this securely
MY_IV = "qswUlsJEkWCcRXClFgz51Q=="
CryptKeeper::Provider::AesNew.prepend Module.new {
def encrypt(value)
AES.encrypt(value, key, iv: MY_IV)
end
}
Use a separate hash column
You can store a hash of the plaintext in a separate column and check that for uniqueness:
class User
validates_uniqueness_of :name_hash
before_validation do
self.name_hash = Digest::SHA1.hexdigest(name)
end
end
Manually validate
If your table is small and you can afford a full-table scan you could do something like:
class User
validate :unique_name
def unique_name
used = self.class.all.any? do |user|
user.name == name
end
errors.add :name, "is already taken" if used
end
end
Perhaps this is expected behavior and I'm missing something. Having issues with certain validations failing once the
crypt_keeper
method is applied to the attribute. For example I have a model which defines encryption for thetitle
attribute:Once I add the
title
attribute to crypt_keeper the uniqueness validation will fail. Below is the validation:Rspec test syntax:
Test output when actually run with rspec:
Any insight of how I can properly test the uniqueness of this attribute and still provide encryption?