ankane / kms_encrypted

Simple, secure key management for Lockbox and attr_encrypted
MIT License
248 stars 24 forks source link

Using KMS with new Rails encrypted ActiveModel functionality #33

Closed robinvdvleuten closed 3 years ago

robinvdvleuten commented 3 years ago

Now that Rails support encrypting ActiveModel attributes (https://github.com/rails/rails/pull/41659, https://github.com/rails/rails/blob/main/guides/source/active_record_encryption.md), would it be possible to use that functionality with KMS managed keys as well? Maybe this gem could expose a custom key provider?

ankane commented 3 years ago

Hey @robinvdvleuten, thanks for the suggestion. I put together a simple key provider for AWS (code at end). I think it's probably better as a separate gem since it'll be pretty different from the existing functionality. One limitation is there doesn't appear to be a way to get info about the record being encrypted for the encryption context (which is very useful for auditing). Also, multiple attributes can't use the same data key, so it's a bit less flexible.

class KmsKeyProvider
  def initialize(key_id:)
    @key_id = key_id
  end

  def encryption_key
    data_key = ActiveRecord::Encryption.key_generator.generate_random_key
    encrypted_data_key = client.encrypt(key_id: @key_id, plaintext: data_key).ciphertext_blob

    key = ActiveRecord::Encryption::Key.new(data_key)
    key.public_tags.encrypted_data_key = encrypted_data_key
    key
  end

  def decryption_keys(encrypted_message)
    encrypted_data_key = encrypted_message.headers.encrypted_data_key
    data_key = client.decrypt(ciphertext_blob: encrypted_data_key).plaintext
    [ActiveRecord::Encryption::Key.new(data_key)]
  end

  private

  def client
    @client ||= Aws::KMS::Client.new
  end
end
butsjoh commented 3 years ago

Hi,

Thnx for sharing the example above. I am wondering what your thoughts are on the rails suggested implementation of encrypted attributes. We are also looking into it and indeed there seems to be no easy way to know which record is being encrypted so passing any context is not really possible. We like that rails is providing default abilities for this but it seems it is focussed on getting the master keys from the configuration to try to decrypt data (which is in contradiction of what kms_encrypted does "Master encryption keys are not on application servers"). I don't see with its current implementation how it would work if the KMS master key gets rotated that it for already encrypted data figure out the previous keys without passing them through the configuration. We are still actively exploring this and maybe my assumptions above are wrong cause i did not do any coding work yet and i just reading documentation. My understanding of kms_encrypted was that it would be possible to not have any master key information passed in the configuration.

ankane commented 3 years ago

Hey @butsjoh, there shouldn't be much difference in terms of the envelope encryption. With the key provider above, you shouldn't need additional keys (Rails will require you to set them, but won't use them).

butsjoh commented 3 years ago

So you are saying that at decryption time if you send a encrypted_data_key that what encrypted with a key that already has been rotated (in our case we use vault) that it will still decrypt correctly? It does not need to know the context or previous versions?

ankane commented 3 years ago

You can write a key provider in a way that keeps track of all the info needed to decrypt (same as how KMS Encrypted works), so there shouldn't be a difference between the two there.

butsjoh commented 3 years ago

Ok i see but my actual question was if you in the example key provider you gave above (in that case AWS) would rotate the master key would the code needed to be adapted or not?

ankane commented 3 years ago

No, you wouldn't need any code changes. The encrypted data key has all the info AWS needs to decrypt (even if you change the key_id). For Vault, you'd need to store both the encrypted data key and the key_id.

Here's what Vault would look like:

class KmsKeyProvider
  def initialize(key_id:)
    @key_id = key_id
  end

  def encryption_key
    data_key = ActiveRecord::Encryption.key_generator.generate_random_key
    encrypted_data_key = client.logical.write("transit/encrypt/#{@key_id}", plaintext: Base64.encode64(data_key)).data[:ciphertext]

    key = ActiveRecord::Encryption::Key.new(data_key)
    key.public_tags.encrypted_data_key = encrypted_data_key
    key.public_tags.encrypted_data_key_id = @key_id
    key
  end

  def decryption_keys(encrypted_message)
    encrypted_data_key = encrypted_message.headers.encrypted_data_key
    key_id = encrypted_message.headers.encrypted_data_key_id
    data_key = Base64.decode64(client.logical.write("transit/decrypt/#{key_id}", ciphertext: encrypted_data_key).data[:plaintext])
    [ActiveRecord::Encryption::Key.new(data_key)]
  end

  private

  def client
    @client ||= Vault::Client.new
  end
end
butsjoh commented 3 years ago

Thnx ! I understand it know.

butsjoh commented 3 years ago

@ankane I have been looking more in to detail over the weekend and i do have a question that is more related to the implementation of kms_encrypted. In your initial blog post (https://ankane.org/sensitive-data-rails) you are mentioning the concept of enveloppe encryption and that kms_encrypted is very suitable for that. The blog post also mentions "Another approach is envelope encryption, which prevents the KMS from seeing the unencrypted data.". Having looked at the source code of kms_encrypted it seems the data that is going to be encrypted is being send along to the encryption client (in our case we use vault, it does forget it though) so i guess that statement was a general one and not applicable for kms_encrypted? So from my understanding (please proof me wrong if so :)) encryption of the data is done by the encryption client and not in ruby. I guess that was a design choice of kms_encrypted?

It seems vault has abilities to generate a data key of your own which you then can use locally in ruby to encrypt your data. See https://learn.hashicorp.com/tutorials/vault/eaas-transit#generate-data-key as a reference. So i guess the implementation you showed above using vault and encrypting the key with the transit/encrypt/ and transit/decrypt endpoints could also be swapped out by using the transit/datakey endpoint and storing the encrypted datakey in the encrypted message headers or am missing something?

ankane commented 3 years ago

KMS Encrypted uses envelope encryption as described in that blog post and the readme. It generates the data key in Ruby rather than calling the KMS to do it so there's more flexibility in when the KMS call occurs.

For the KmsKeyProvider above, you can probably use the transit/datakey endpoint as well.

butsjoh commented 3 years ago

Hi,

I see know :) I forgot to lock how it integrates with lockbox and see now how you are using the encrypt and decrypt endpoint in the transit engine to encrypt the key. Any reason why you choose to implemented it like that and not use the transit/datakey endpoint to generate the key? I guess it is very similar in approach?

Thnx for the support on these gems.

ankane commented 3 years ago

They should be similar in many cases. In the example above, encrypt should generalize better. ActiveRecord::Encryption.key_generator.generate_random_key returns a 256-bit key by default, but could return a key of any length. The datakey endpoint would need to be aware of the key length you want.

ankane commented 2 years ago

Just fyi: I packaged this approach into a new gem.