Closed robinvdvleuten closed 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
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.
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).
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?
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.
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?
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
Thnx ! I understand it know.
@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?
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.
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.
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.
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?