kms_encrypted 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bec6f5799315832f67c01e6ffe79b58a5c5019e3ca8efa5a2c0795149a2b727c
4
- data.tar.gz: 8cbea27c197f2da4dbd1cad3bbee59c36c2404cd8eddb379c07be7ebbfc72a05
3
+ metadata.gz: 11c711222f049df42c663c9e68a2822fb7e318d3f3fc02f6346801c998ead328
4
+ data.tar.gz: 98200a87d8bf02a7398b075aaa6d79dff706a4d15d850584ebd952329980520b
5
5
  SHA512:
6
- metadata.gz: ba30a61a50245321fcca825ce5d36a4f350edb26b832f033d029b6051578a1f6d3ac8efb81f01339762a1a6869f6e70ca4a4fc780d3341ea80b4d17ca57724e8
7
- data.tar.gz: e511299714c5d035f000d7e85de6855be813fccc0a65fe7332a9418e0dc2733c6618ae42070f582877b48db98117aabc9b605ab1931537885180b20693fb7da0
6
+ metadata.gz: 7b3d37f2739f1a869a8a34c62e9300c3b54b0e8b87b98377883fa252d161aaafcb387044049bf0bfb070e86d481034bcf5c19b0e83d21a4668a513b02f3da151
7
+ data.tar.gz: b5596bc03466ac15b09d72cd1e00093eda05e2dcd3ca17607484ff678da808647bb7d364be27b5cfc912cdcccb991cbb55b18fd94206a4cb4510d17eaeb4c0da
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 1.0.0
2
+
3
+ - Added versioning
4
+ - Added `context_hash` method
5
+
6
+ Breaking changes
7
+
8
+ - There’s now a default encryption context with the model name and id
9
+ - ActiveSupport notifications were changed from `generate_data_key` and `decrypt_data_key` to `encrypt` and `decrypt`
10
+ - AWS KMS uses the `Encrypt` operation instead of `GenerateDataKey`
11
+
1
12
  ## 0.3.0
2
13
 
3
14
  - Added support for Vault
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2017 Andrew Kane
1
+ Copyright (c) 2017-2018 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -2,39 +2,66 @@
2
2
 
3
3
  Simple, secure key management for [attr_encrypted](https://github.com/attr-encrypted/attr_encrypted)
4
4
 
5
- The attr_encrypted gem is great for encryption, but:
5
+ With KMS Encrypted:
6
6
 
7
- 1. Leaves you to manage the security of your keys
8
- 2. Doesnt provide an easy way to rotate your keys
9
- 3. Doesn’t have a great audit trail to see how data has been accessed
10
- 4. Doesn’t let you grant encryption and decryption permission separately
7
+ - Master encryption keys are not on application servers
8
+ - Theres an immutable audit log of all activity
9
+ - Decryption can be disabled if an attack is detected
10
+ - Encrypt and decrypt permissions can be granted separately
11
+ - It’s easy to rotate keys
11
12
 
12
- Key management services address all of these issues and it’s easy to use them together.
13
-
14
- Supports [Amazon KMS](https://aws.amazon.com/kms/), [Google KMS](https://cloud.google.com/kms/), and [Vault](https://www.vaultproject.io/)
13
+ Supports [AWS KMS](https://aws.amazon.com/kms/), [Google Cloud KMS](https://cloud.google.com/kms/), and [Vault](https://www.vaultproject.io/)
15
14
 
16
15
  [![Build Status](https://travis-ci.org/ankane/kms_encrypted.svg?branch=master)](https://travis-ci.org/ankane/kms_encrypted)
17
16
 
18
17
  ## How It Works
19
18
 
20
- This approach uses KMS to manage encryption keys and attr_encrypted to do the encryption.
19
+ This approach uses a key management service (KMS) to manage encryption keys and attr_encrypted to do the encryption.
21
20
 
22
- To encrypt an attribute, we first generate a data key and encrypt it with KMS. This is known as [envelope encryption](https://cloud.google.com/kms/docs/envelope-encryption). We pass the unencrypted version to attr_encrypted and store the encrypted version in the `encrypted_kms_key` column. For each record, we generate a different data key.
21
+ To encrypt an attribute, we first generate a data key and encrypt it with the KMS. This is known as [envelope encryption](https://cloud.google.com/kms/docs/envelope-encryption). We pass the unencrypted version to attr_encrypted and store the encrypted version in the `encrypted_kms_key` column. For each record, we generate a different data key.
23
22
 
24
- To decrypt an attribute, we first decrypt the data key with KMS. Once we have the decrypted key, we pass it to attr_encrypted to decrypt the data. We can easily track decryptions since we have a different data key for each record.
23
+ To decrypt an attribute, we first decrypt the data key with the KMS. Once we have the decrypted key, we pass it to attr_encrypted to decrypt the data. We can easily track decryptions since we have a different data key for each record.
25
24
 
26
25
  ## Getting Started
27
26
 
28
27
  Follow the instructions for your key management service:
29
28
 
30
- - [Amazon KMS](guides/Amazon.md)
31
- - [Google KMS](guides/Google.md)
29
+ - [AWS KMS](guides/Amazon.md)
30
+ - [Google Cloud KMS](guides/Google.md)
32
31
  - [Vault](guides/Vault.md)
33
32
 
34
33
  ## Related Projects
35
34
 
36
35
  To securely search encrypted data, check out [Blind Index](https://github.com/ankane/blind_index).
37
36
 
37
+ ## Upgrading
38
+
39
+ ### 1.0
40
+
41
+ KMS Encrypted 1.0 brings a number of improvements. Here are a few breaking changes to be aware of:
42
+
43
+ - There’s now a default encryption context with the model name and id
44
+ - ActiveSupport notifications were changed from `generate_data_key` and `decrypt_data_key` to `encrypt` and `decrypt`
45
+ - AWS KMS uses the `Encrypt` operation instead of `GenerateDataKey`
46
+
47
+ If you didn’t previously use encryption context, add the `upgrade_context` option to your models:
48
+
49
+ ```ruby
50
+ class User < ApplicationRecord
51
+ has_kms_key upgrade_context: true
52
+ end
53
+ ```
54
+
55
+ Then run:
56
+
57
+ ```ruby
58
+ User.where("encrypted_kms_key NOT LIKE 'v1:%'").find_each do |user|
59
+ user.rotate_kms_key!
60
+ end
61
+ ```
62
+
63
+ And remove the `upgrade_context` option.
64
+
38
65
  ## History
39
66
 
40
67
  View the [changelog](CHANGELOG.md)
@@ -0,0 +1,69 @@
1
+ module KmsEncrypted
2
+ class Client
3
+ delegate :encrypt, :decrypt, to: :client
4
+
5
+ attr_reader :key_id, :data_key
6
+
7
+ def initialize(key_id: nil, legacy_context: false, data_key: false)
8
+ @key_id = key_id || ENV["KMS_KEY_ID"]
9
+ @legacy_context = legacy_context
10
+ @data_key = data_key
11
+ end
12
+
13
+ def encrypt(plaintext, context: nil)
14
+ event = {
15
+ key_id: key_id,
16
+ context: context,
17
+ data_key: data_key
18
+ }
19
+
20
+ ActiveSupport::Notifications.instrument("encrypt.kms_encrypted", event) do
21
+ client.encrypt(plaintext, context: context)
22
+ end
23
+ end
24
+
25
+ def decrypt(ciphertext, context: nil)
26
+ event = {
27
+ key_id: key_id,
28
+ context: context,
29
+ data_key: data_key
30
+ }
31
+
32
+ ActiveSupport::Notifications.instrument("decrypt.kms_encrypted", event) do
33
+ client.decrypt(ciphertext, context: context)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def provider
40
+ if key_id == "insecure-test-key"
41
+ :test
42
+ elsif key_id.start_with?("vault/")
43
+ :vault
44
+ elsif key_id.start_with?("projects/")
45
+ :google
46
+ else
47
+ :aws
48
+ end
49
+ end
50
+
51
+ def client
52
+ @client ||= begin
53
+ klass =
54
+ case provider
55
+ when :test
56
+ KmsEncrypted::Clients::Test
57
+ when :vault
58
+ KmsEncrypted::Clients::Vault
59
+ when :google
60
+ KmsEncrypted::Clients::Google
61
+ else
62
+ KmsEncrypted::Clients::Aws
63
+ end
64
+
65
+ klass.new(key_id: key_id, legacy_context: @legacy_context)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,36 @@
1
+ module KmsEncrypted
2
+ module Clients
3
+ class Aws < Base
4
+ def encrypt(plaintext, context: nil)
5
+ options = {
6
+ key_id: key_id,
7
+ plaintext: plaintext
8
+ }
9
+ options[:encryption_context] = generate_context(context) if context
10
+
11
+ KmsEncrypted.aws_client.encrypt(options).ciphertext_blob
12
+ end
13
+
14
+ def decrypt(ciphertext, context: nil)
15
+ options = {
16
+ ciphertext_blob: ciphertext
17
+ }
18
+ options[:encryption_context] = generate_context(context) if context
19
+
20
+ begin
21
+ KmsEncrypted.aws_client.decrypt(options).plaintext
22
+ rescue ::Aws::KMS::Errors::InvalidCiphertextException
23
+ decryption_failed!
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ # make integers strings for convenience
30
+ def generate_context(context)
31
+ raise ArgumentError, "Context must be a hash" unless context.is_a?(Hash)
32
+ Hash[context.map { |k, v| [k, context_value(v)] }]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,45 @@
1
+ module KmsEncrypted
2
+ module Clients
3
+ class Base
4
+ attr_reader :key_id
5
+
6
+ def initialize(key_id: nil, legacy_context: false)
7
+ @key_id = key_id
8
+ @legacy_context = legacy_context
9
+ end
10
+
11
+ protected
12
+
13
+ def decryption_failed!
14
+ raise DecryptionError, "Decryption failed"
15
+ end
16
+
17
+ # keys must be ordered consistently
18
+ # values are checked for validity
19
+ # then converted to strings
20
+ def generate_context(context)
21
+ if @legacy_context
22
+ context.to_json
23
+ elsif context.is_a?(Hash)
24
+ Hash[context.sort_by { |k| k.to_s }.map { |k, v| [context_key(k), context_value(v)] }].to_json
25
+ else
26
+ context
27
+ end
28
+ end
29
+
30
+ def context_key(k)
31
+ unless k.is_a?(String) || k.is_a?(Symbol)
32
+ raise ArgumentError, "Context keys must be a string or symbol"
33
+ end
34
+ k.to_s
35
+ end
36
+
37
+ def context_value(v)
38
+ unless v.is_a?(String) || v.is_a?(Integer)
39
+ raise ArgumentError, "Context values must be a string or integer"
40
+ end
41
+ v.to_s
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,40 @@
1
+ module KmsEncrypted
2
+ module Clients
3
+ class Google < Base
4
+ attr_reader :last_key_version
5
+
6
+ def encrypt(plaintext, context: nil)
7
+ options = {
8
+ plaintext: plaintext
9
+ }
10
+ options[:additional_authenticated_data] = generate_context(context) if context
11
+
12
+ # ensure namespace gets loaded
13
+ client = KmsEncrypted.google_client
14
+ request = ::Google::Apis::CloudkmsV1::EncryptRequest.new(options)
15
+ response = client.encrypt_crypto_key(key_id, request)
16
+
17
+ @last_key_version = response.name
18
+
19
+ response.ciphertext
20
+ end
21
+
22
+ def decrypt(ciphertext, context: nil)
23
+ options = {
24
+ ciphertext: ciphertext
25
+ }
26
+ options[:additional_authenticated_data] = generate_context(context) if context
27
+
28
+ # ensure namespace gets loaded
29
+ client = KmsEncrypted.google_client
30
+ request = ::Google::Apis::CloudkmsV1::DecryptRequest.new(options)
31
+ begin
32
+ client.decrypt_crypto_key(key_id, request).plaintext
33
+ rescue ::Google::Apis::ClientError => e
34
+ decryption_failed! if e.message.include?("Decryption failed")
35
+ raise e
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ module KmsEncrypted
2
+ module Clients
3
+ class Test < Base
4
+ PREFIX = Base64.decode64("insecure+data+A")
5
+
6
+ def encrypt(plaintext, context: nil)
7
+ parts = [PREFIX, Base64.strict_encode64(plaintext)]
8
+ parts << generate_context(context) if context
9
+ parts.join(":")
10
+ end
11
+
12
+ def decrypt(ciphertext, context: nil)
13
+ prefix, plaintext, stored_context = ciphertext.split(":")
14
+
15
+ context = generate_context(context) if context
16
+ decryption_failed! if context != stored_context
17
+
18
+ Base64.decode64(plaintext)
19
+ end
20
+
21
+ private
22
+
23
+ # turn hash into json
24
+ def generate_context(context)
25
+ Base64.encode64(super)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ module KmsEncrypted
2
+ module Clients
3
+ class Vault < Base
4
+ def encrypt(plaintext, context: nil)
5
+ options = {
6
+ plaintext: Base64.encode64(plaintext)
7
+ }
8
+ options[:context] = generate_context(context) if context
9
+
10
+ response = KmsEncrypted.vault_client.logical.write(
11
+ "transit/encrypt/#{key_id.sub("vault/", "")}",
12
+ options
13
+ )
14
+
15
+ response.data[:ciphertext]
16
+ end
17
+
18
+ def decrypt(ciphertext, context: nil)
19
+ options = {
20
+ ciphertext: ciphertext
21
+ }
22
+ options[:context] = generate_context(context) if context
23
+
24
+ response =
25
+ begin
26
+ KmsEncrypted.vault_client.logical.write(
27
+ "transit/decrypt/#{key_id.sub("vault/", "")}",
28
+ options
29
+ )
30
+ rescue ::Vault::HTTPClientError => e
31
+ decryption_failed! if e.message.include?("unable to decrypt")
32
+ raise e
33
+ rescue Encoding::UndefinedConversionError
34
+ decryption_failed!
35
+ end
36
+
37
+ Base64.decode64(response.data[:plaintext])
38
+ end
39
+
40
+ private
41
+
42
+ # turn hash into json
43
+ def generate_context(context)
44
+ Base64.encode64(super)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,93 @@
1
+ module KmsEncrypted
2
+ class Database
3
+ attr_reader :record, :key_method, :options
4
+
5
+ def initialize(record, key_method)
6
+ @record = record
7
+ @key_method = key_method
8
+ @options = record.class.kms_keys[key_method.to_sym]
9
+ end
10
+
11
+ def name
12
+ options[:name]
13
+ end
14
+
15
+ def current_version
16
+ @version ||= begin
17
+ version = options[:version]
18
+ version = record.instance_exec(&version) if version.respond_to?(:call)
19
+ version.to_i
20
+ end
21
+ end
22
+
23
+ def key_version(version)
24
+ versions = (options[:previous_versions] || {}).dup
25
+ versions[current_version] ||= options.slice(:key_id)
26
+
27
+ raise KmsEncrypted::Error, "Version not active: #{version}" unless versions[version]
28
+
29
+ key_id = versions[version][:key_id]
30
+
31
+ raise ArgumentError, "Missing key id" unless key_id
32
+
33
+ key_id
34
+ end
35
+
36
+ def context(version)
37
+ context_method = name ? "kms_encryption_context_#{name}" : "kms_encryption_context"
38
+ if record.method(context_method).arity == 0
39
+ record.send(context_method)
40
+ else
41
+ record.send(context_method, version: version)
42
+ end
43
+ end
44
+
45
+ def encrypt(plaintext)
46
+ key_id = key_version(current_version)
47
+ context = context(current_version)
48
+ ciphertext = KmsEncrypted::Client.new(key_id: key_id, data_key: true).encrypt(plaintext, context: context)
49
+ "v#{current_version}:#{encode64(ciphertext)}"
50
+ end
51
+
52
+ def decrypt(ciphertext)
53
+ m = /\Av(\d+):/.match(ciphertext)
54
+ if m
55
+ version = m[1].to_i
56
+ ciphertext = ciphertext.sub("v#{version}:", "")
57
+ else
58
+ version = 1
59
+ context = {} if options[:upgrade_context]
60
+ legacy_context = true
61
+
62
+ # legacy
63
+ if ciphertext.start_with?("$gc$")
64
+ _, _, short_key_id, ciphertext = ciphertext.split("$", 4)
65
+
66
+ # restore key, except for cryptoKeyVersion
67
+ stored_key_id = decode64(short_key_id).split("/")[0..3]
68
+ stored_key_id.insert(0, "projects")
69
+ stored_key_id.insert(2, "locations")
70
+ stored_key_id.insert(4, "keyRings")
71
+ stored_key_id.insert(6, "cryptoKeys")
72
+ key_id = stored_key_id.join("/")
73
+ elsif ciphertext.start_with?("vault:")
74
+ ciphertext = Base64.encode64(ciphertext)
75
+ end
76
+ end
77
+
78
+ key_id ||= key_version(version)
79
+ context ||= context(version)
80
+ ciphertext = decode64(ciphertext)
81
+
82
+ KmsEncrypted::Client.new(key_id: key_id, data_key: true, legacy_context: legacy_context).decrypt(ciphertext, context: context)
83
+ end
84
+
85
+ def encode64(bytes)
86
+ Base64.strict_encode64(bytes)
87
+ end
88
+
89
+ def decode64(bytes)
90
+ Base64.decode64(bytes)
91
+ end
92
+ end
93
+ end
@@ -1,17 +1,25 @@
1
1
  module KmsEncrypted
2
2
  class LogSubscriber < ActiveSupport::LogSubscriber
3
- def decrypt_data_key(event)
3
+ def decrypt(event)
4
4
  return unless logger.debug?
5
5
 
6
- name = "Decrypt Data Key (#{event.duration.round(1)}ms)"
7
- debug " #{color(name, YELLOW, true)} Context: #{event.payload[:context].inspect}"
6
+ data_key = event.payload[:data_key]
7
+ name = data_key ? "Decrypt Data Key" : "Decrypt"
8
+ name += " (#{event.duration.round(1)}ms)"
9
+ context = event.payload[:context]
10
+ context = context.inspect if context.is_a?(Hash)
11
+ debug " #{color(name, YELLOW, true)} Context: #{context}"
8
12
  end
9
13
 
10
- def generate_data_key(event)
14
+ def encrypt(event)
11
15
  return unless logger.debug?
12
16
 
13
- name = "Generate Data Key (#{event.duration.round(1)}ms)"
14
- debug " #{color(name, YELLOW, true)} Context: #{event.payload[:context].inspect}"
17
+ data_key = event.payload[:data_key]
18
+ name = data_key ? "Encrypt Data Key" : "Encrypt"
19
+ name += " (#{event.duration.round(1)}ms)"
20
+ context = event.payload[:context]
21
+ context = context.inspect if context.is_a?(Hash)
22
+ debug " #{color(name, YELLOW, true)} Context: #{context}"
15
23
  end
16
24
  end
17
25
  end