kms_encrypted 0.3.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -7
- data/LICENSE.txt +1 -1
- data/README.md +499 -15
- data/lib/kms_encrypted.rb +27 -2
- data/lib/kms_encrypted/box.rb +91 -0
- data/lib/kms_encrypted/client.rb +69 -0
- data/lib/kms_encrypted/clients/aws.rb +36 -0
- data/lib/kms_encrypted/clients/base.rb +45 -0
- data/lib/kms_encrypted/clients/google.rb +40 -0
- data/lib/kms_encrypted/clients/test.rb +29 -0
- data/lib/kms_encrypted/clients/vault.rb +48 -0
- data/lib/kms_encrypted/database.rb +63 -0
- data/lib/kms_encrypted/log_subscriber.rb +14 -6
- data/lib/kms_encrypted/model.rb +143 -128
- data/lib/kms_encrypted/version.rb +1 -1
- metadata +45 -19
- data/.gitignore +0 -9
- data/.travis.yml +0 -11
- data/Gemfile +0 -3
- data/Rakefile +0 -11
- data/guides/Amazon.md +0 -262
- data/guides/Google.md +0 -131
- data/guides/Vault.md +0 -143
- data/kms_encrypted.gemspec +0 -34
@@ -1,17 +1,25 @@
|
|
1
1
|
module KmsEncrypted
|
2
2
|
class LogSubscriber < ActiveSupport::LogSubscriber
|
3
|
-
def
|
3
|
+
def decrypt(event)
|
4
4
|
return unless logger.debug?
|
5
5
|
|
6
|
-
|
7
|
-
|
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
|
14
|
+
def encrypt(event)
|
11
15
|
return unless logger.debug?
|
12
16
|
|
13
|
-
|
14
|
-
|
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
|
data/lib/kms_encrypted/model.rb
CHANGED
@@ -1,158 +1,173 @@
|
|
1
1
|
module KmsEncrypted
|
2
2
|
module Model
|
3
|
-
def has_kms_key(
|
4
|
-
key_id ||=
|
3
|
+
def has_kms_key(name: nil, key_id: nil, eager_encrypt: false, version: 1, previous_versions: nil, upgrade_context: false)
|
4
|
+
key_id ||= ENV["KMS_KEY_ID"]
|
5
5
|
|
6
6
|
key_method = name ? "kms_key_#{name}" : "kms_key"
|
7
|
+
key_column = "encrypted_#{key_method}"
|
8
|
+
context_method = name ? "kms_encryption_context_#{name}" : "kms_encryption_context"
|
7
9
|
|
8
10
|
class_eval do
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
@kms_keys ||= {}
|
12
|
+
|
13
|
+
unless respond_to?(:kms_keys)
|
14
|
+
def self.kms_keys
|
15
|
+
parent_keys =
|
16
|
+
if superclass.respond_to?(:kms_keys)
|
17
|
+
superclass.kms_keys
|
18
|
+
else
|
19
|
+
{}
|
20
|
+
end
|
21
|
+
|
22
|
+
parent_keys.merge(@kms_keys || {})
|
23
|
+
end
|
13
24
|
end
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
25
|
+
|
26
|
+
@kms_keys[key_method.to_sym] = {
|
27
|
+
key_id: key_id,
|
28
|
+
name: name,
|
29
|
+
version: version,
|
30
|
+
previous_versions: previous_versions,
|
31
|
+
upgrade_context: upgrade_context
|
32
|
+
}
|
33
|
+
|
34
|
+
if @kms_keys.size == 1
|
35
|
+
after_save :encrypt_kms_keys
|
36
|
+
|
37
|
+
# fetch all keys together so only need to update database once
|
38
|
+
def encrypt_kms_keys
|
39
|
+
updates = {}
|
40
|
+
self.class.kms_keys.each do |key_method, key|
|
41
|
+
instance_var = "@#{key_method}"
|
42
|
+
key_column = "encrypted_#{key_method}"
|
43
|
+
plaintext_key = instance_variable_get(instance_var)
|
44
|
+
|
45
|
+
if !send(key_column) && plaintext_key
|
46
|
+
updates[key_column] = KmsEncrypted::Database.new(self, key_method).encrypt(plaintext_key)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
if updates.any?
|
50
|
+
current_time = current_time_from_proper_timezone
|
51
|
+
timestamp_attributes_for_update_in_model.each do |attr|
|
52
|
+
updates[attr] = current_time
|
53
|
+
end
|
54
|
+
update_columns(updates)
|
23
55
|
end
|
24
|
-
|
56
|
+
end
|
57
|
+
|
58
|
+
if method_defined?(:reload)
|
59
|
+
m = Module.new do
|
60
|
+
define_method(:reload) do |*args, &block|
|
61
|
+
result = super(*args, &block)
|
62
|
+
self.class.kms_keys.keys.each do |key_method|
|
63
|
+
instance_variable_set("@#{key_method}", nil)
|
64
|
+
end
|
65
|
+
result
|
66
|
+
end
|
67
|
+
end
|
68
|
+
prepend m
|
25
69
|
end
|
26
70
|
end
|
27
71
|
|
28
72
|
define_method(key_method) do
|
29
|
-
raise ArgumentError, "Missing key id" unless key_id
|
30
|
-
|
31
73
|
instance_var = "@#{key_method}"
|
32
74
|
|
33
75
|
unless instance_variable_get(instance_var)
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
key_id: key_id,
|
45
|
-
context: context
|
46
|
-
}
|
47
|
-
ActiveSupport::Notifications.instrument("generate_data_key.kms_encrypted", event) do
|
48
|
-
if key_id == "insecure-test-key"
|
49
|
-
plaintext_key = "00000000000000000000000000000000"
|
50
|
-
encrypted_key = "insecure-data-key-#{rand(1_000_000_000_000)}"
|
51
|
-
elsif key_id.start_with?("projects/")
|
52
|
-
# generate random AES-256 key
|
53
|
-
plaintext_key = OpenSSL::Random.random_bytes(32)
|
54
|
-
|
55
|
-
# encrypt it
|
56
|
-
# load client first to ensure namespace is loaded
|
57
|
-
client = KmsEncrypted.google_client
|
58
|
-
request = ::Google::Apis::CloudkmsV1::EncryptRequest.new(
|
59
|
-
plaintext: plaintext_key,
|
60
|
-
additional_authenticated_data: context.to_json
|
61
|
-
)
|
62
|
-
response = client.encrypt_crypto_key(key_id, request)
|
63
|
-
key_version = response.name
|
64
|
-
|
65
|
-
# shorten key to save space
|
66
|
-
short_key_id = Base64.encode64(key_version.split("/").select.with_index { |_, i| i.odd? }.join("/"))
|
67
|
-
|
68
|
-
# build encrypted key
|
69
|
-
# we reference the key in the field for easy rotation
|
70
|
-
encrypted_key = "$gc$#{short_key_id}$#{[response.ciphertext].pack(default_encoding)}"
|
71
|
-
elsif key_id.start_with?("vault/")
|
72
|
-
# generate random AES-256 key
|
73
|
-
plaintext_key = OpenSSL::Random.random_bytes(32)
|
74
|
-
|
75
|
-
# encrypt it
|
76
|
-
response = KmsEncrypted.vault_client.logical.write(
|
77
|
-
"transit/encrypt/#{key_id.sub("vault/", "")}",
|
78
|
-
plaintext: Base64.encode64(plaintext_key),
|
79
|
-
context: Base64.encode64(context.to_json)
|
80
|
-
)
|
81
|
-
|
82
|
-
encrypted_key = response.data[:ciphertext]
|
83
|
-
else
|
84
|
-
# generate data key from API
|
85
|
-
resp = KmsEncrypted.aws_client.generate_data_key(
|
86
|
-
key_id: key_id,
|
87
|
-
encryption_context: context,
|
88
|
-
key_spec: "AES_256"
|
89
|
-
)
|
90
|
-
plaintext_key = resp.plaintext
|
91
|
-
encrypted_key = [resp.ciphertext_blob].pack(default_encoding)
|
76
|
+
encrypted_key = send(key_column)
|
77
|
+
plaintext_key =
|
78
|
+
if encrypted_key
|
79
|
+
KmsEncrypted::Database.new(self, key_method).decrypt(encrypted_key)
|
80
|
+
else
|
81
|
+
key = SecureRandom.random_bytes(32)
|
82
|
+
|
83
|
+
if eager_encrypt == :fetch_id
|
84
|
+
raise ArgumentError, ":fetch_id only works with Postgres" unless self.class.connection.adapter_name =~ /postg/i
|
85
|
+
self.id ||= self.class.connection.execute("select nextval('#{self.class.sequence_name}')").first["nextval"]
|
92
86
|
end
|
93
|
-
end
|
94
87
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
unless instance_variable_get(instance_var)
|
100
|
-
encrypted_key = send(key_column)
|
101
|
-
plaintext_key = nil
|
102
|
-
|
103
|
-
event = {
|
104
|
-
key_id: key_id,
|
105
|
-
context: context
|
106
|
-
}
|
107
|
-
ActiveSupport::Notifications.instrument("decrypt_data_key.kms_encrypted", event) do
|
108
|
-
if encrypted_key.start_with?("insecure-data-key-")
|
109
|
-
plaintext_key = "00000000000000000000000000000000".encode("BINARY")
|
110
|
-
elsif encrypted_key.start_with?("$gc$")
|
111
|
-
_, _, short_key_id, ciphertext = encrypted_key.split("$", 4)
|
112
|
-
|
113
|
-
# restore key, except for cryptoKeyVersion
|
114
|
-
stored_key_id = Base64.decode64(short_key_id).split("/")[0..3]
|
115
|
-
stored_key_id.insert(0, "projects")
|
116
|
-
stored_key_id.insert(2, "locations")
|
117
|
-
stored_key_id.insert(4, "keyRings")
|
118
|
-
stored_key_id.insert(6, "cryptoKeys")
|
119
|
-
stored_key_id = stored_key_id.join("/")
|
120
|
-
|
121
|
-
# load client first to ensure namespace is loaded
|
122
|
-
client = KmsEncrypted.google_client
|
123
|
-
request = ::Google::Apis::CloudkmsV1::DecryptRequest.new(
|
124
|
-
ciphertext: ciphertext.unpack(default_encoding).first,
|
125
|
-
additional_authenticated_data: context.to_json
|
126
|
-
)
|
127
|
-
plaintext_key = client.decrypt_crypto_key(stored_key_id, request).plaintext
|
128
|
-
elsif encrypted_key.start_with?("vault:")
|
129
|
-
response = KmsEncrypted.vault_client.logical.write(
|
130
|
-
"transit/decrypt/#{key_id.sub("vault/", "")}",
|
131
|
-
ciphertext: encrypted_key,
|
132
|
-
context: Base64.encode64(context.to_json)
|
133
|
-
)
|
134
|
-
|
135
|
-
plaintext_key = Base64.decode64(response.data[:plaintext])
|
136
|
-
else
|
137
|
-
plaintext_key = KmsEncrypted.aws_client.decrypt(
|
138
|
-
ciphertext_blob: encrypted_key.unpack(default_encoding).first,
|
139
|
-
encryption_context: context
|
140
|
-
).plaintext
|
88
|
+
if eager_encrypt == true || ([:try, :fetch_id].include?(eager_encrypt) && id)
|
89
|
+
encrypted_key = KmsEncrypted::Database.new(self, key_method).encrypt(key)
|
90
|
+
send("#{key_column}=", encrypted_key)
|
141
91
|
end
|
142
|
-
end
|
143
92
|
|
144
|
-
|
145
|
-
|
93
|
+
key
|
94
|
+
end
|
95
|
+
instance_variable_set(instance_var, plaintext_key)
|
146
96
|
end
|
147
97
|
|
148
98
|
instance_variable_get(instance_var)
|
149
99
|
end
|
150
100
|
|
101
|
+
define_method(context_method) do
|
102
|
+
raise KmsEncrypted::Error, "id needed for encryption context" unless id
|
103
|
+
|
104
|
+
{
|
105
|
+
model_name: model_name.to_s,
|
106
|
+
model_id: id
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
# automatically detects attributes and files where the encryption key is:
|
111
|
+
# 1. a symbol that matches kms key method exactly
|
112
|
+
# does not detect attributes and files where the encryption key is:
|
113
|
+
# 1. callable (warns)
|
114
|
+
# 2. a symbol that internally calls kms key method
|
115
|
+
# it could try to get the exact key and compare
|
116
|
+
# (there's a very small chance this could have false positives)
|
117
|
+
# but bias towards simplicity for now
|
118
|
+
# TODO possibly raise error for callable keys in 2.0
|
119
|
+
# with option to override/specify attributes
|
151
120
|
define_method("rotate_#{key_method}!") do
|
152
121
|
# decrypt
|
153
122
|
plaintext_attributes = {}
|
154
|
-
|
155
|
-
|
123
|
+
|
124
|
+
# attr_encrypted
|
125
|
+
if self.class.respond_to?(:encrypted_attributes)
|
126
|
+
self.class.encrypted_attributes.each do |key, v|
|
127
|
+
if v[:key] == key_method.to_sym
|
128
|
+
plaintext_attributes[key] = send(key)
|
129
|
+
elsif v[:key].respond_to?(:call)
|
130
|
+
warn "[kms_encrypted] Can't detect if encrypted attribute uses this key"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# lockbox attributes
|
136
|
+
# only checks key, not previous versions
|
137
|
+
if self.class.respond_to?(:lockbox_attributes)
|
138
|
+
self.class.lockbox_attributes.each do |key, v|
|
139
|
+
if v[:key] == key_method.to_sym
|
140
|
+
plaintext_attributes[key] = send(key)
|
141
|
+
elsif v[:key].respond_to?(:call)
|
142
|
+
warn "[kms_encrypted] Can't detect if encrypted attribute uses this key"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# lockbox attachments
|
148
|
+
# only checks key, not previous versions
|
149
|
+
if self.class.respond_to?(:lockbox_attachments)
|
150
|
+
self.class.lockbox_attachments.each do |key, v|
|
151
|
+
if v[:key] == key_method.to_sym
|
152
|
+
# can likely add support at some point, but may be complicated
|
153
|
+
# ideally use rotate_encryption! from Lockbox
|
154
|
+
# but needs access to both old and new keys
|
155
|
+
# also need to update database atomically
|
156
|
+
raise KmsEncrypted::Error, "Can't rotate key used for encrypted files"
|
157
|
+
elsif v[:key].respond_to?(:call)
|
158
|
+
warn "[kms_encrypted] Can't detect if encrypted attachment uses this key"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# CarrierWave uploaders
|
164
|
+
if self.class.respond_to?(:uploaders)
|
165
|
+
self.class.uploaders.each do |_, uploader|
|
166
|
+
# for simplicity, only checks if key is callable
|
167
|
+
if uploader.respond_to?(:lockbox_options) && uploader.lockbox_options[:key].respond_to?(:call)
|
168
|
+
warn "[kms_encrypted] Can't detect if encrypted uploader uses this key"
|
169
|
+
end
|
170
|
+
end
|
156
171
|
end
|
157
172
|
|
158
173
|
# reset key
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kms_encrypted
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-08-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '5'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,6 +108,20 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: lockbox
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 0.4.7
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 0.4.7
|
111
125
|
- !ruby/object:Gem::Dependency
|
112
126
|
name: aws-sdk-kms
|
113
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -150,25 +164,38 @@ dependencies:
|
|
150
164
|
- - ">="
|
151
165
|
- !ruby/object:Gem::Version
|
152
166
|
version: '0'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: carrierwave
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
153
181
|
description:
|
154
|
-
email:
|
155
|
-
- andrew@chartkick.com
|
182
|
+
email: andrew@chartkick.com
|
156
183
|
executables: []
|
157
184
|
extensions: []
|
158
185
|
extra_rdoc_files: []
|
159
186
|
files:
|
160
|
-
- ".gitignore"
|
161
|
-
- ".travis.yml"
|
162
187
|
- CHANGELOG.md
|
163
|
-
- Gemfile
|
164
188
|
- LICENSE.txt
|
165
189
|
- README.md
|
166
|
-
- Rakefile
|
167
|
-
- guides/Amazon.md
|
168
|
-
- guides/Google.md
|
169
|
-
- guides/Vault.md
|
170
|
-
- kms_encrypted.gemspec
|
171
190
|
- lib/kms_encrypted.rb
|
191
|
+
- lib/kms_encrypted/box.rb
|
192
|
+
- lib/kms_encrypted/client.rb
|
193
|
+
- lib/kms_encrypted/clients/aws.rb
|
194
|
+
- lib/kms_encrypted/clients/base.rb
|
195
|
+
- lib/kms_encrypted/clients/google.rb
|
196
|
+
- lib/kms_encrypted/clients/test.rb
|
197
|
+
- lib/kms_encrypted/clients/vault.rb
|
198
|
+
- lib/kms_encrypted/database.rb
|
172
199
|
- lib/kms_encrypted/log_subscriber.rb
|
173
200
|
- lib/kms_encrypted/model.rb
|
174
201
|
- lib/kms_encrypted/version.rb
|
@@ -184,16 +211,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
184
211
|
requirements:
|
185
212
|
- - ">="
|
186
213
|
- !ruby/object:Gem::Version
|
187
|
-
version: '
|
214
|
+
version: '2.4'
|
188
215
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
189
216
|
requirements:
|
190
217
|
- - ">="
|
191
218
|
- !ruby/object:Gem::Version
|
192
219
|
version: '0'
|
193
220
|
requirements: []
|
194
|
-
|
195
|
-
rubygems_version: 2.7.7
|
221
|
+
rubygems_version: 3.1.2
|
196
222
|
signing_key:
|
197
223
|
specification_version: 4
|
198
|
-
summary: Simple, secure key management for attr_encrypted
|
224
|
+
summary: Simple, secure key management for Lockbox and attr_encrypted
|
199
225
|
test_files: []
|