Skip to content

Commit b96ddea

Browse files
Merge pull request rails#46284 from jonathanhefner/active_record-ciphertext_for-not-yet-encrypted
Fix `ciphertext_for` for yet-to-be-encrypted values
2 parents 1f039d8 + 02e351e commit b96ddea

File tree

5 files changed

+89
-22
lines changed

5 files changed

+89
-22
lines changed

activerecord/CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,37 @@
1+
* Fix `ciphertext_for` for yet-to-be-encrypted values.
2+
3+
Previously, `ciphertext_for` returned the cleartext of values that had not
4+
yet been encrypted, such as with an unpersisted record:
5+
6+
```ruby
7+
Post.encrypts :body
8+
9+
post = Post.create!(body: "Hello")
10+
post.ciphertext_for(:body)
11+
# => "{\"p\":\"abc..."
12+
13+
post.body = "World"
14+
post.ciphertext_for(:body)
15+
# => "World"
16+
```
17+
18+
Now, `ciphertext_for` will always return the ciphertext of encrypted
19+
attributes:
20+
21+
```ruby
22+
Post.encrypts :body
23+
24+
post = Post.create!(body: "Hello")
25+
post.ciphertext_for(:body)
26+
# => "{\"p\":\"abc..."
27+
28+
post.body = "World"
29+
post.ciphertext_for(:body)
30+
# => "{\"p\":\"xyz..."
31+
```
32+
33+
*Jonathan Hefner*
34+
135
* Fix a bug where using groups and counts with long table names would return incorrect results.
236

337
*Shota Toguchi*, *Yusaku Ono*

activerecord/lib/active_record/encryption/encryptable_record.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,16 @@ def validate_column_size(attribute_name)
140140

141141
# Returns whether a given attribute is encrypted or not.
142142
def encrypted_attribute?(attribute_name)
143-
ActiveRecord::Encryption.encryptor.encrypted? ciphertext_for(attribute_name)
143+
ActiveRecord::Encryption.encryptor.encrypted? read_attribute_before_type_cast(attribute_name)
144144
end
145145

146146
# Returns the ciphertext for +attribute_name+.
147147
def ciphertext_for(attribute_name)
148-
read_attribute_before_type_cast(attribute_name)
148+
if encrypted_attribute?(attribute_name)
149+
read_attribute_before_type_cast(attribute_name)
150+
else
151+
read_attribute_for_database(attribute_name)
152+
end
149153
end
150154

151155
# Encrypts all the encryptable attributes and saves the changes.

activerecord/test/cases/encryption/contexts_test.rb

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,26 @@ class ActiveRecord::Encryption::ContextsTest < ActiveRecord::EncryptionTestCase
1111
ActiveRecord::Encryption.config.support_unencrypted_data = true
1212

1313
@post = EncryptedPost.create!(title: "Some encrypted post title", body: "Some body")
14-
@clean_title = @post.title
14+
@title_cleartext = @post.title
15+
@title_ciphertext = @post.ciphertext_for(:title)
1516
end
1617

1718
test ".with_encryption_context lets you override properties" do
1819
ActiveRecord::Encryption.with_encryption_context(encryptor: ActiveRecord::Encryption::NullEncryptor.new) do
19-
assert_protected_encrypted_attribute(@post, :title, @clean_title)
20+
assert_equal @title_ciphertext, @post.reload.title
21+
2022
@post.update!(title: "Some new title")
2123
end
2224

23-
assert_equal "Some new title", @post.title
25+
assert_equal "Some new title", @post.title_before_type_cast
2426
end
2527

2628
test ".with_encryption_context will restore previous context properties when there is an error" do
2729
ActiveRecord::Encryption.with_encryption_context(encryptor: ActiveRecord::Encryption::NullEncryptor.new) do
2830
raise "Some error"
2931
end
3032
rescue
31-
assert_encrypted_attribute @post.reload, :title, @clean_title
33+
assert_encrypted_attribute @post.reload, :title, @title_cleartext
3234
end
3335

3436
test ".with_encryption_context can be nested multiple times" do
@@ -51,17 +53,17 @@ class ActiveRecord::Encryption::ContextsTest < ActiveRecord::EncryptionTestCase
5153

5254
test ".without_encryption won't decrypt or encrypt data automatically" do
5355
ActiveRecord::Encryption.without_encryption do
54-
assert_protected_encrypted_attribute(@post, :title, @clean_title)
56+
assert_equal @title_ciphertext, @post.reload.title
5557

5658
@post.update!(title: "Some new title")
5759
end
5860

59-
assert_equal "Some new title", @post.title
61+
assert_not_encrypted_attribute @post, :title, "Some new title"
6062
end
6163

6264
test ".protecting_encrypted_data don't decrypt attributes automatically" do
6365
ActiveRecord::Encryption.protecting_encrypted_data do
64-
assert_protected_encrypted_attribute(@post, :title, @clean_title)
66+
assert_equal @title_ciphertext, @post.reload.title
6567
end
6668
end
6769

@@ -92,10 +94,4 @@ class ActiveRecord::Encryption::ContextsTest < ActiveRecord::EncryptionTestCase
9294
end
9395
end
9496
end
95-
96-
private
97-
def assert_protected_encrypted_attribute(model, attribute_name, clean_value)
98-
assert_equal model.reload.ciphertext_for(attribute_name), model.public_send(attribute_name)
99-
assert_not_equal clean_value, model.ciphertext_for(:title)
100-
end
10197
end

activerecord/test/cases/encryption/encryptable_record_api_test.rb

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,40 @@ class ActiveRecord::Encryption::EncryptableRecordApiTest < ActiveRecord::Encrypt
7373

7474
test "encrypted_attribute? returns false for encrypted attributes which content is not encrypted" do
7575
book = ActiveRecord::Encryption.without_encryption { EncryptedBook.create!(name: "Dune") }
76-
assert_not book.encrypted_attribute?(:title)
76+
assert_not book.encrypted_attribute?(:name)
7777
end
7878

79-
test "ciphertext_for returns the chiphertext for a given attributes" do
79+
test "ciphertext_for returns the ciphertext for a given attribute" do
8080
book = EncryptedBook.create!(name: "Dune")
8181

82-
assert_equal book.ciphertext_for(:name), book.ciphertext_for(:name)
83-
assert_not_equal book.name, book.ciphertext_for(:name)
82+
assert_ciphertext_decrypts_to book, :name, book.ciphertext_for(:name)
83+
end
84+
85+
test "ciphertext_for returns the persisted ciphertext for a non-deterministically encrypted attribute" do
86+
post = EncryptedPost.create!(title: "Fear is the mind-killer", body: "Fear is the little-death...")
87+
88+
assert_equal post.title_before_type_cast, post.ciphertext_for(:title)
89+
assert_ciphertext_decrypts_to post, :title, post.ciphertext_for(:title)
90+
end
91+
92+
test "ciphertext_for returns the ciphertext of a new value" do
93+
book = EncryptedBook.create!(name: "Dune")
94+
book.name = "Arrakis"
95+
96+
assert_ciphertext_decrypts_to book, :name, book.ciphertext_for(:name)
97+
end
98+
99+
test "ciphertext_for returns the ciphertext of a decrypted value" do
100+
book = EncryptedBook.create!(name: "Dune")
101+
book.decrypt
102+
103+
assert_ciphertext_decrypts_to book, :name, book.ciphertext_for(:name)
104+
end
105+
106+
test "ciphertext_for returns the ciphertext of a value when the record is new" do
107+
book = EncryptedBook.new(name: "Dune")
108+
109+
assert_ciphertext_decrypts_to book, :name, book.ciphertext_for(:name)
84110
end
85111

86112
test "encrypt won't change the encoding of strings even when compression is used" do

activerecord/test/cases/encryption/helper.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@ class ActiveRecord::Fixture
99

1010
module ActiveRecord::Encryption
1111
module EncryptionHelpers
12+
def assert_ciphertext_decrypts_to(model, attribute_name, ciphertext)
13+
assert_not_equal model.public_send(attribute_name), ciphertext
14+
assert_not_equal model.read_attribute(attribute_name), ciphertext
15+
cleartext = model.type_for_attribute(attribute_name).deserialize(ciphertext)
16+
assert_equal model.read_attribute(attribute_name), cleartext
17+
end
18+
1219
def assert_encrypted_attribute(model, attribute_name, expected_value)
13-
assert_not_equal expected_value, model.ciphertext_for(attribute_name)
20+
assert_ciphertext_decrypts_to model, attribute_name, model.read_attribute_before_type_cast(attribute_name)
1421
assert_equal expected_value, model.public_send(attribute_name)
1522
unless model.new_record?
1623
model.reload
17-
assert_not_equal expected_value, model.ciphertext_for(attribute_name)
24+
assert_ciphertext_decrypts_to model, attribute_name, model.read_attribute_before_type_cast(attribute_name)
1825
assert_equal expected_value, model.public_send(attribute_name)
1926
end
2027
end
@@ -29,7 +36,7 @@ def assert_invalid_key_cant_read_attribute(model, attribute_name)
2936

3037
def assert_not_encrypted_attribute(model, attribute_name, expected_value)
3138
assert_equal expected_value, model.send(attribute_name)
32-
assert_equal expected_value, model.ciphertext_for(attribute_name)
39+
assert_equal expected_value, model.read_attribute_before_type_cast(attribute_name)
3340
end
3441

3542
def assert_encrypted_record(model)

0 commit comments

Comments
 (0)