attr-encrypted / attr_encrypted

Generates attr_accessors that encrypt and decrypt attributes
MIT License
2.01k stars 427 forks source link

How to re-encrypt? #109

Closed lewang closed 8 years ago

lewang commented 10 years ago

I've inherited a project where attr-encrypted is used. If I need to change encryption keys OR algorithm, how do I do that?

IOW, I need to write a migration that decrypts using the current key/algo and re-encrypt using the new. There does not appear an API beneath the magical layer that allows for this?

saghaulor commented 10 years ago

Hi @lewang, that's a great question. I myself have been tossing around potential solutions to key rotation. Perhaps we could solve the problem together and maybe reveal ways to make the gem more friendly towards these sorts of operations.

The key can be changed fairly easily, you just need to call a proc instead of using a static string. I did something like the following:

  attr_encrypted :number, :key => :encryption_key, :attribute => :encrypted_number, :mode => :per_attribute_iv_and_salt

  def self.encryption_key
    key = proc { some_logic_that_reveals_the_key }
    key.call
  end

  def encryption_key
    self.class.encryption_key
  end

Then the key can be changed by re-evaluating the initialized object.

So rotating the key is fairly trivial, the problem is re-encrypting existing data with the new key, assuming that you do it all at once. If you don't re-encrypt everything all at once, rather, you do it opportunistically, well then you have a problem of knowing which keys were used for what records, you'll have to store some metadata about the key used for encryption with each record.

Changing the crypto algorithm is trickier. If it's a permanent change to the algorithm, change it in the model per the documentation https://github.com/attr-encrypted/attr_encrypted#custom-algorithms, if you have a situation where some records are encrypted with one algorithm, and some with another, then you can decrypt attributes at the class level by passing in the algorithm and encryption/decryption methods as documented here https://github.com/attr-encrypted/attr_encrypted/blob/master/lib/attr_encrypted.rb#L192

As you have probably guessed, this is not a seamless experience. Key rotation or changing algorithms will either need to be done through a migration or a rake task. If you're subject to security compliance requirements like PCI, you may want to do the key rotation in a rake task as you'll have to do it annually, it doesn't really make sense to have a migration annually.

Ideally there would be an interface such that you inject these options at the instance level for seamless attribute decryption, but I'm not sure how such a thing would be implemented.

I hope that this helps. I welcome any ideas that you have that would improve on my suggestions.

lewang commented 10 years ago

To clarify your strategy:

  1. Load all instances of Foo
  2. Change key by redefining self.encryption_key
  3. Save all instances of Foo

That seems reasonable. Honestly, I'm just tired of all this Rails gemification of programming. I wouldn't use a gem for this at all. Just make a PORO encryptor object and pass that around. Code would be so much simpler to understand.

saghaulor commented 10 years ago

To be clear, you'll want to create a hash where you decrypt the values with the original key, because once you change the key you won't be able to read the encrypted attribute, you'll encounter a Bad Decrypt error. Then change the key and use the hash to save the attributes.

lewang commented 10 years ago

Doesn't load also decrypt? I guess I'll just experiment.

barrywark commented 9 years ago

It might be easier to use a blue/green approach. If you can spare the database columns, having two encrypted attributes (password_blue and password_green, for example), would allow you to migrate between the columns, using a separate key for each. At the app level, you could keep state variable that stores the current column (blue or green). Models would select the appropriate column based on state. Key rotation is then just copying the values from one column to the other (assuming both columns' keys are set) and then clearing the old value.

abglassman commented 9 years ago

An approach like this one from the Fernet library in python might work: provide a list of configurations (a list of option maps, for example, or an option legacy_options: [{algorithm: 'blahblah256', key: 'oldkey'...},{...},...{...}] - all encryption could be performed with the current set of options, decryption could be performed by trying the legacy configurations in sequence until something decrypts to a value that validates.

saghaulor commented 8 years ago

@lewang I don't know if you ever figured out how to achieve this, but work done in v3 makes this much easier. Re-encryption involves taking advantage of options evaluation at the instance level. https://github.com/attr-encrypted/attr_encrypted#options-are-evaluated

Essentially you'll create a class with functions that switch the options when decrypting and when encrypting. Then you'll cycle through your records with that class and migrate everything to the new implementation.

class MigrateUserCrypto < ActiveRecord::Base
  self.table_name('users')

  attr_encrypted :ssn, key: key_toggle(:ssn), mode: mode_toggle(:ssn), algorithm: algorithm_toggle(:ssn), insecure_mode: is_decrypting?(:ssn)

  def is_decrypting?(attribute)
    encrypted_attributes[attribute][:operation] == :decrypting
  end

  def mode_toggle(attribute)
    if is_decrypting?(attribute)
      :single_iv_and_salt
    else
      :per_attribute_iv
    end
  end

  def algorithm_toggle(attribute)
    if is_decrypting?(attribute)
      'aes-256-cbc'
    else
      'aes-256-gcm'
    end
  end

  def key_toggle(attribute)
    if is_decrypting?(attribute)
      old_encryption_key
    else
      new_encryption_key
    end
  end
end

MigrateUserCrypto.all.each do |user|
  old_ssn = user.ssn
  user.ssn= old_ssn
  user.save
end
hale commented 7 years ago

FWIW I had to use proc as well, and the table name config is now a setter: self.table_name = "users"

This is in Rails 4 - maybe the code above is for Rails 3?

rakibulislam commented 7 years ago

@saghaulor I am following your example to create an instance method to toggle the key. Here is my code:

class MigrateAccountCrypto < ActiveRecord::Base
  self.table_name = 'accounts'

  attr_encrypted :login, key: key_toggle(:login)
  attr_encrypted :password, key: key_toggle(:password)

  def is_decrypting?(attribute)
    encrypted_attributes[attribute][:operation] == :decrypting
  end

  def key_toggle(attribute)
    if is_decrypting?(attribute)
      Rails.application.secrets.attr_encrypted_key
    else
      Rails.application.secrets.new_attr_encrypted_key
    end
  end
end

But, I am getting this error:

NoMethodError: undefined method `key_toggle' for #<Class:0x007fdeb2d055c8>

meaning that the instance method key_toggle as an option for attr_encrypted :attribute is not really working. I am using attr_encrypted-3.0.3. Is there anything that I am missing or I can try to make it work?

Thanks!

saghaulor commented 7 years ago

@rakibulislam I believe the way you're passing in the key_toggle method, the key_toggle is actually being called when the class is loading. I could be wrong.

Perhaps try this:

 attr_encrypted :login, key: :key_toggle(:login)
 attr_encrypted :password, key: :key_toggle(:password)
rakibulislam commented 7 years ago

@saghaulor Thanks a lot for getting back to me. Actually key: :key_toggle(:login) is not a valid syntax, so I tried this way:

  attr_encrypted :login, key: send(:key_toggle, :login)
  attr_encrypted :password, key: send(:key_toggle, :password)

But, still getting the same error:

rake aborted!
NoMethodError: undefined method `key_toggle' for #<Class:0x007fc83b0106c0>

It only works if the key_toggle method has no argument:

attr_encrypted :login, key: :key_toggle

But, I need to send the attribute as the argument of the key_toggle(:attribute) method like you showed in your example above. Any thought?

saghaulor commented 7 years ago

@rakibulislam sorry about that, I thought that it might be invalid syntax.

You could always pass it as a proc.

  attr_encrypted :login, key: Proc.new { |account| account.key_toggle( :login) }
  attr_encrypted :password, key: Proc.new { |account| account.key_toggle(:password) }

Hopefully that does what you need.

rakibulislam commented 7 years ago

@saghaulor awesome! That works perfectly :-) Thanks a bunch for the help 💯 ✋