Skip to content

Commit

Permalink
Merge pull request #99 from hashicorp/VLTE-single_decrypt
Browse files Browse the repository at this point in the history
vault_single_decrypt flag to allow for the decryption of single attributes
  • Loading branch information
Lauren Voswinkel authored May 11, 2020
2 parents 13f8689 + a8e1eba commit 1fec86c
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 6 deletions.
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ vault_attribute :credit_card,
- **Note** Changing this value for an existing application will make existing values no longer decryptable!

#### Lazy attribute decryption
By default, `vault-rails` will decrypt a record’s encrypted attributes on that record’s initializarion. You can configure an encrypted model to decrypt attributes lazily, which will prevent communication with Vault until an encrypted attribute’s getter method is called, at which point all of the record’s encrypted attributes will be decrypted. This is useful if you do not always need access to encrypted attributes. For example:
By default, `vault-rails` will decrypt a record’s encrypted attributes on that record’s initialization. You can configure an encrypted model to decrypt attributes lazily, which will prevent communication with Vault until an encrypted attribute’s getter method is called, at which point all of the record’s encrypted attributes will be decrypted. This is useful if you do not always need access to encrypted attributes. For example:


```ruby
Expand All @@ -202,6 +202,33 @@ person.ssn # Vault communication happens here
# => "123-45-6789"
```

#### Single, lazy attribute decryption
By default, `vault-rails` will decrypt all encrypted attributes on that record’s initialization on a class by class basis. You can configure an encrypted model to decrypt attributes lazily and and individually. This will prevent vault from loading all vault_attributes defined on a class the moment one attribute is requested.


```ruby
class Person < ActiveRecord::Base
include Vault::EncryptedModel
vault_lazy_decrypt!
vault_single_decrypt!
vault_attribute :ssn
vault_attribute :email
end
# Without vault_single_decrypt:
person = Person.find(id) # Vault communication happens here
person.ssn # Vault communication happens here, fetches both ssn and email
# => "123-45-6789"
# With vault_single_decrypt:
person = Person.find(id)
person.ssn # Vault communication happens here, fetches only ssn
# => "123-45-6789"
person.email # Vault communication happens here, fetches only email
# => "[email protected]"
```

#### Serialization

By default, all values are assumed to be "text" fields in the database. Sometimes it is beneficial for your application to work with a more flexible data structure (such as a Hash or Array). Vault-rails can automatically serialize and deserialize these structures for you:
Expand Down
23 changes: 18 additions & 5 deletions lib/vault/encrypted_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ def vault_attribute(attribute, options = {})

# Getter
define_method("#{attribute}") do
self.__vault_load_attributes! unless @__vault_loaded
self.__vault_load_attributes!(attribute) unless @__vault_loaded
super()
end

# Setter
define_method("#{attribute}=") do |value|
self.__vault_load_attributes! unless @__vault_loaded
self.__vault_load_attributes!(attribute) unless @__vault_loaded

# We always set it as changed without comparing with the current value
# because we allow our held values to be mutated, so we need to assume
Expand All @@ -94,7 +94,8 @@ def vault_attribute(attribute, options = {})

# Checker
define_method("#{attribute}?") do
public_send(attribute).present?
self.__vault_load_attributes!(attribute) unless @__vault_loaded
instance_variable_get("@#{attribute}").present?
end

# Make a note of this attribute so we can use it in the future (maybe).
Expand Down Expand Up @@ -152,6 +153,14 @@ def vault_lazy_decrypt
def vault_lazy_decrypt!
@vault_lazy_decrypt = true
end

def vault_single_decrypt
@vault_single_decrypt ||= false
end

def vault_single_decrypt!
@vault_single_decrypt = true
end
end

included do
Expand All @@ -178,12 +187,16 @@ def __vault_initialize_attributes!
__vault_load_attributes!
end

def __vault_load_attributes!
def __vault_load_attributes!(attribute_to_read = nil)
self.class.__vault_attributes.each do |attribute, options|
# skip loading certain keys in one of two cases:
# 1- the attribute has already been loaded
# 2- the single decrypt option is set AND this is not the attribute we're requesting to decrypt
next if instance_variable_get("@#{attribute}") || (self.class.vault_single_decrypt && attribute_to_read != attribute)
self.__vault_load_attribute!(attribute, options)
end

@__vault_loaded = true
@__vault_loaded = self.class.__vault_attributes.all? { |attribute, __| instance_variable_get("@#{attribute}") }

return true
end
Expand Down
18 changes: 18 additions & 0 deletions spec/dummy/app/models/lazy_single_person.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

class LazySinglePerson < ActiveRecord::Base
include Vault::EncryptedModel

self.table_name = "people"

vault_lazy_decrypt!
vault_single_decrypt!

vault_attribute :ssn

vault_attribute :credit_card,
encrypted_column: :cc_encrypted

def encryption_context
"user_#{id}"
end
end
125 changes: 125 additions & 0 deletions spec/integration/rails_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,131 @@
end
end

context "lazy single decrypt" do
before(:all) do
Vault::Rails.logical.write("transit/keys/dummy_people_ssn")
end

it "encrypts attributes" do
person = LazySinglePerson.create!(ssn: "123-45-6789")
expect(person.ssn_encrypted.length).to eq(61)
expect(person.ssn_encrypted).to start_with("vault:v1:")
expect(person.ssn_encrypted.encoding).to eq(Encoding::UTF_8)
end

it "decrypts attributes" do
person = LazySinglePerson.create!(ssn: "123-45-6789")
person.reload

expect(person.ssn).to eq("123-45-6789")
expect(person.ssn.encoding).to eq(Encoding::UTF_8)
end

it "does not decrypt on initialization" do
person = LazySinglePerson.create!(ssn: "123-45-6789")
person.reload

p2 = LazySinglePerson.find(person.id)

expect(p2.instance_variable_get("@ssn")).to eq(nil)
expect(p2.ssn).to eq("123-45-6789")
end

it "does not decrypt all attributes on single read" do
person = LazySinglePerson.create!(ssn: "123-45-6789")
person.update_attributes!(credit_card: "abcd-efgh-hijk-lmno")
expect(person.credit_card).to eq("abcd-efgh-hijk-lmno")

person.reload

p2 = LazySinglePerson.find(person.id)

expect(p2.instance_variable_get("@ssn")).to eq(nil)
expect(p2.ssn).to eq("123-45-6789")
expect(p2.instance_variable_get("@credit_card")).to eq(nil)
expect(p2.credit_card).to eq("abcd-efgh-hijk-lmno")
end

it "does not decrypt all attributes on single write" do
person = LazySinglePerson.create!(ssn: "123-45-6789")
person.update_attributes!(credit_card: "abcd-efgh-hijk-lmno")
expect(person.credit_card).to eq("abcd-efgh-hijk-lmno")

person.reload

p2 = LazySinglePerson.find(person.id)

expect(p2.instance_variable_get("@ssn")).to eq(nil)
expect(p2.ssn).to eq("123-45-6789")
person.ssn = "111-11-1111"
expect(p2.instance_variable_get("@credit_card")).to eq(nil)
expect(p2.credit_card).to eq("abcd-efgh-hijk-lmno")
end

it "tracks dirty attributes" do
person = LazySinglePerson.create!(ssn: "123-45-6789")

expect(person.ssn_changed?).to be(false)
expect(person.ssn_change).to be(nil)
expect(person.ssn_was).to eq("123-45-6789")

person.ssn = "111-11-1111"

expect(person.ssn_changed?).to be(true)
expect(person.ssn_change).to eq(["123-45-6789", "111-11-1111"])
expect(person.ssn_was).to eq("123-45-6789")
end

it "allows attributes to be unset" do
person = LazySinglePerson.create!(ssn: "123-45-6789")
person.update_attributes!(ssn: nil)
person.reload

expect(person.ssn).to be(nil)
end

it "allows saving without validations" do
person = LazySinglePerson.new(ssn: "123-456-7890")
expect(person.save(validate: false)).to be(true)
expect(person.ssn_encrypted).to match("vault:")
end

it "allows attributes to be unset after reload" do
person = LazySinglePerson.create!(ssn: "123-45-6789")
person.reload
person.update_attributes!(ssn: nil)
person.reload

expect(person.ssn).to be(nil)
end

it "allows attributes to be blank" do
person = LazySinglePerson.create!(ssn: "123-45-6789")
person.update_attributes!(ssn: "")
person.reload

expect(person.ssn).to eq("")
end

it "reloads instance variables on reload" do
person = LazySinglePerson.create!(ssn: "123-45-6789")
expect(person.instance_variable_get(:@ssn)).to eq("123-45-6789")

person.ssn = "111-11-1111"
person.reload

expect(person.ssn).to eq("123-45-6789")
end

it "does not try to encrypt unchanged attributes" do
person = LazySinglePerson.create!(ssn: "123-45-6789")

expect(Vault::Rails).to_not receive(:encrypt)
person.name = "Cinderella"
person.save!
end
end

context "with custom options" do
before(:all) do
Vault::Rails.sys.mount("credit-secrets", :transit)
Expand Down

0 comments on commit 1fec86c

Please sign in to comment.