kms_encrypted 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +0 -2
- data/CHANGELOG.md +6 -0
- data/README.md +6 -1
- data/guides/Amazon.md +3 -3
- data/guides/Google.md +1 -1
- data/guides/Vault.md +143 -0
- data/kms_encrypted.gemspec +1 -0
- data/lib/kms_encrypted/model.rb +30 -6
- data/lib/kms_encrypted/version.rb +1 -1
- data/lib/kms_encrypted.rb +21 -51
- metadata +18 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bec6f5799315832f67c01e6ffe79b58a5c5019e3ca8efa5a2c0795149a2b727c
|
4
|
+
data.tar.gz: 8cbea27c197f2da4dbd1cad3bbee59c36c2404cd8eddb379c07be7ebbfc72a05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba30a61a50245321fcca825ce5d36a4f350edb26b832f033d029b6051578a1f6d3ac8efb81f01339762a1a6869f6e70ca4a4fc780d3341ea80b4d17ca57724e8
|
7
|
+
data.tar.gz: e511299714c5d035f000d7e85de6855be813fccc0a65fe7332a9418e0dc2733c6618ae42070f582877b48db98117aabc9b605ab1931537885180b20693fb7da0
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
## 0.3.0
|
2
|
+
|
3
|
+
- Added support for Vault
|
4
|
+
- Removed `KmsEncrypted.kms_client` and `KmsEncrypted.client_options` in favor of `KmsEncrypted.aws_client`
|
5
|
+
- Removed `KmsEncrypted::Google.kms_client` in favor of `KmsEncrypted.google_client`
|
6
|
+
|
1
7
|
## 0.2.0
|
2
8
|
|
3
9
|
- Added support for Google KMS
|
data/README.md
CHANGED
@@ -11,7 +11,7 @@ The attr_encrypted gem is great for encryption, but:
|
|
11
11
|
|
12
12
|
Key management services address all of these issues and it’s easy to use them together.
|
13
13
|
|
14
|
-
Supports [Amazon KMS](https://aws.amazon.com/kms/)
|
14
|
+
Supports [Amazon KMS](https://aws.amazon.com/kms/), [Google KMS](https://cloud.google.com/kms/), and [Vault](https://www.vaultproject.io/)
|
15
15
|
|
16
16
|
[![Build Status](https://travis-ci.org/ankane/kms_encrypted.svg?branch=master)](https://travis-ci.org/ankane/kms_encrypted)
|
17
17
|
|
@@ -29,6 +29,11 @@ Follow the instructions for your key management service:
|
|
29
29
|
|
30
30
|
- [Amazon KMS](guides/Amazon.md)
|
31
31
|
- [Google KMS](guides/Google.md)
|
32
|
+
- [Vault](guides/Vault.md)
|
33
|
+
|
34
|
+
## Related Projects
|
35
|
+
|
36
|
+
To securely search encrypted data, check out [Blind Index](https://github.com/ankane/blind_index).
|
32
37
|
|
33
38
|
## History
|
34
39
|
|
data/guides/Amazon.md
CHANGED
@@ -57,7 +57,7 @@ class User < ApplicationRecord
|
|
57
57
|
end
|
58
58
|
```
|
59
59
|
|
60
|
-
The context is used as part of the encryption and decryption process, so it must be a value that doesn’t change. Otherwise, you won’t be able to decrypt. Read more about [encryption context here](
|
60
|
+
The context is used as part of the encryption and decryption process, so it must be a value that doesn’t change. Otherwise, you won’t be able to decrypt. Read more about [encryption context here](https://docs.aws.amazon.com/kms/latest/developerguide/encryption-context.html).
|
61
61
|
|
62
62
|
The primary key is a good choice, but auto-generated ids aren’t available until a record is created, and we need to encrypt before this. One solution is to preload the primary key. Here’s what it looks like with Postgres:
|
63
63
|
|
@@ -147,7 +147,7 @@ We recommend setting up alerts on suspicious behavior.
|
|
147
147
|
|
148
148
|
## Key Rotation
|
149
149
|
|
150
|
-
KMS supports [automatic key rotation](
|
150
|
+
KMS supports [automatic key rotation](https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html). No action is required in this case.
|
151
151
|
|
152
152
|
To manually rotate keys, replace the old KMS key id with the new key id in your model. Your app does not need the old key id to perform rotation (however, the key must still be enabled in your AWS account).
|
153
153
|
|
@@ -259,4 +259,4 @@ user.rotate_kms_key_phone!
|
|
259
259
|
|
260
260
|
## File Uploads
|
261
261
|
|
262
|
-
While outside the scope of this gem, you can also use KMS for sensitive file uploads. Check out [this guide](https://
|
262
|
+
While outside the scope of this gem, you can also use KMS for sensitive file uploads. Check out [this guide](https://ankane.org/aws-client-side-encryption) to learn more.
|
data/guides/Google.md
CHANGED
data/guides/Vault.md
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
# Vault
|
2
|
+
|
3
|
+
Add this line to your application’s Gemfile:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
gem 'vault'
|
7
|
+
gem 'kms_encrypted'
|
8
|
+
```
|
9
|
+
|
10
|
+
Add columns for the encrypted data and the encrypted KMS data keys
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
add_column :users, :encrypted_email, :text
|
14
|
+
add_column :users, :encrypted_email_iv, :text
|
15
|
+
add_column :users, :encrypted_kms_key, :text
|
16
|
+
```
|
17
|
+
|
18
|
+
Enable the [transit](https://www.vaultproject.io/docs/secrets/transit/index.html) backend
|
19
|
+
|
20
|
+
```sh
|
21
|
+
vault secrets enable transit
|
22
|
+
```
|
23
|
+
|
24
|
+
And create a key
|
25
|
+
|
26
|
+
```sh
|
27
|
+
vault write -f transit/keys/my-key
|
28
|
+
```
|
29
|
+
|
30
|
+
Set it in your environment along with your Vault credentials ([dotenv](https://github.com/bkeepers/dotenv) is great for this)
|
31
|
+
|
32
|
+
```sh
|
33
|
+
KMS_KEY_ID=vault/my-key
|
34
|
+
VAULT_ADDR=http://127.0.0.1:8200
|
35
|
+
VAULT_TOKEN=secret
|
36
|
+
```
|
37
|
+
|
38
|
+
And update your model
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class User < ApplicationRecord
|
42
|
+
has_kms_key
|
43
|
+
|
44
|
+
attr_encrypted :email, key: :kms_key
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
For each encrypted attribute, use the `kms_key` method for its key.
|
49
|
+
|
50
|
+
## Auditing
|
51
|
+
|
52
|
+
Follow the [instructions here](https://www.vaultproject.io/docs/audit/) to set up data access logging. To know what data is being decrypted, you’ll need to add context.
|
53
|
+
|
54
|
+
Add a `kms_encryption_context` method to your model.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class User < ApplicationRecord
|
58
|
+
def kms_encryption_context
|
59
|
+
# some hash
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
The context is used as part of the encryption and decryption process, so it must be a value that doesn’t change. Otherwise, you won’t be able to decrypt. Read more about [encryption context here](https://docs.aws.amazon.com/kms/latest/developerguide/encryption-context.html).
|
65
|
+
|
66
|
+
The primary key is a good choice, but auto-generated ids aren’t available until a record is created, and we need to encrypt before this. One solution is to preload the primary key. Here’s what it looks like with Postgres:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
class User < ApplicationRecord
|
70
|
+
def kms_encryption_context
|
71
|
+
self.id ||= self.class.connection.execute("select nextval('#{self.class.sequence_name}')").first["nextval"]
|
72
|
+
{"Record" => "#{model_name}/#{id}"}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
## Alerting
|
78
|
+
|
79
|
+
We recommend setting up alerts on suspicious behavior.
|
80
|
+
|
81
|
+
## Key Rotation
|
82
|
+
|
83
|
+
To manually rotate keys, replace the old key id with the new key id in your model.
|
84
|
+
|
85
|
+
```sh
|
86
|
+
KMS_KEY_ID=...
|
87
|
+
```
|
88
|
+
|
89
|
+
and run
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
User.find_each do |user|
|
93
|
+
user.rotate_kms_key!
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
## Testing
|
98
|
+
|
99
|
+
For testing, you can prevent network calls to KMS by setting:
|
100
|
+
|
101
|
+
```sh
|
102
|
+
KMS_KEY_ID=insecure-test-key
|
103
|
+
```
|
104
|
+
|
105
|
+
## Multiple Keys Per Record
|
106
|
+
|
107
|
+
You may want to protect different columns with different data keys (or even master keys).
|
108
|
+
|
109
|
+
To do this, add more columns
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
add_column :users, :encrypted_phone, :text
|
113
|
+
add_column :users, :encrypted_phone_iv, :text
|
114
|
+
add_column :users, :encrypted_kms_key_phone, :text
|
115
|
+
```
|
116
|
+
|
117
|
+
And update your model
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class User < ApplicationRecord
|
121
|
+
has_kms_key
|
122
|
+
has_kms_key name: :phone, key_id: "..."
|
123
|
+
|
124
|
+
attr_encrypted :email, key: :kms_key
|
125
|
+
attr_encrypted :phone, key: :kms_key_phone
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
For context, use:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
class User < ApplicationRecord
|
133
|
+
def kms_encryption_context_phone
|
134
|
+
# some hash
|
135
|
+
end
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
To rotate keys, use:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
user.rotate_kms_key_phone!
|
143
|
+
```
|
data/kms_encrypted.gemspec
CHANGED
data/lib/kms_encrypted/model.rb
CHANGED
@@ -53,22 +53,36 @@ module KmsEncrypted
|
|
53
53
|
plaintext_key = OpenSSL::Random.random_bytes(32)
|
54
54
|
|
55
55
|
# encrypt it
|
56
|
+
# load client first to ensure namespace is loaded
|
57
|
+
client = KmsEncrypted.google_client
|
56
58
|
request = ::Google::Apis::CloudkmsV1::EncryptRequest.new(
|
57
59
|
plaintext: plaintext_key,
|
58
60
|
additional_authenticated_data: context.to_json
|
59
61
|
)
|
60
|
-
response =
|
62
|
+
response = client.encrypt_crypto_key(key_id, request)
|
61
63
|
key_version = response.name
|
62
64
|
|
63
65
|
# shorten key to save space
|
64
|
-
short_key_id = Base64.encode64(key_version.split("/").select.with_index { |
|
66
|
+
short_key_id = Base64.encode64(key_version.split("/").select.with_index { |_, i| i.odd? }.join("/"))
|
65
67
|
|
66
68
|
# build encrypted key
|
67
69
|
# we reference the key in the field for easy rotation
|
68
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]
|
69
83
|
else
|
70
84
|
# generate data key from API
|
71
|
-
resp = KmsEncrypted.
|
85
|
+
resp = KmsEncrypted.aws_client.generate_data_key(
|
72
86
|
key_id: key_id,
|
73
87
|
encryption_context: context,
|
74
88
|
key_spec: "AES_256"
|
@@ -92,7 +106,7 @@ module KmsEncrypted
|
|
92
106
|
}
|
93
107
|
ActiveSupport::Notifications.instrument("decrypt_data_key.kms_encrypted", event) do
|
94
108
|
if encrypted_key.start_with?("insecure-data-key-")
|
95
|
-
plaintext_key = "00000000000000000000000000000000"
|
109
|
+
plaintext_key = "00000000000000000000000000000000".encode("BINARY")
|
96
110
|
elsif encrypted_key.start_with?("$gc$")
|
97
111
|
_, _, short_key_id, ciphertext = encrypted_key.split("$", 4)
|
98
112
|
|
@@ -104,13 +118,23 @@ module KmsEncrypted
|
|
104
118
|
stored_key_id.insert(6, "cryptoKeys")
|
105
119
|
stored_key_id = stored_key_id.join("/")
|
106
120
|
|
121
|
+
# load client first to ensure namespace is loaded
|
122
|
+
client = KmsEncrypted.google_client
|
107
123
|
request = ::Google::Apis::CloudkmsV1::DecryptRequest.new(
|
108
124
|
ciphertext: ciphertext.unpack(default_encoding).first,
|
109
125
|
additional_authenticated_data: context.to_json
|
110
126
|
)
|
111
|
-
plaintext_key =
|
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])
|
112
136
|
else
|
113
|
-
plaintext_key = KmsEncrypted.
|
137
|
+
plaintext_key = KmsEncrypted.aws_client.decrypt(
|
114
138
|
ciphertext_blob: encrypted_key.unpack(default_encoding).first,
|
115
139
|
encryption_context: context
|
116
140
|
).plaintext
|
data/lib/kms_encrypted.rb
CHANGED
@@ -1,24 +1,6 @@
|
|
1
1
|
# dependencies
|
2
2
|
require "active_support"
|
3
3
|
|
4
|
-
begin
|
5
|
-
# aws-sdk v3
|
6
|
-
require "aws-sdk-kms"
|
7
|
-
rescue LoadError
|
8
|
-
begin
|
9
|
-
# aws-sdk v2
|
10
|
-
require "aws-sdk"
|
11
|
-
rescue LoadError
|
12
|
-
# do nothing
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
begin
|
17
|
-
require "google/apis/cloudkms_v1"
|
18
|
-
rescue LoadError
|
19
|
-
# do nothing
|
20
|
-
end
|
21
|
-
|
22
4
|
# modules
|
23
5
|
require "kms_encrypted/log_subscriber"
|
24
6
|
require "kms_encrypted/model"
|
@@ -26,43 +8,31 @@ require "kms_encrypted/version"
|
|
26
8
|
|
27
9
|
module KmsEncrypted
|
28
10
|
class << self
|
29
|
-
attr_writer :
|
30
|
-
|
31
|
-
|
32
|
-
|
11
|
+
attr_writer :aws_client
|
12
|
+
attr_writer :google_client
|
13
|
+
attr_writer :vault_client
|
14
|
+
|
15
|
+
def aws_client
|
16
|
+
@aws_client ||= Aws::KMS::Client.new(
|
17
|
+
retry_limit: 2,
|
18
|
+
http_open_timeout: 2,
|
19
|
+
http_read_timeout: 2
|
20
|
+
)
|
33
21
|
end
|
34
|
-
alias_method :kms, :kms_client
|
35
22
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
23
|
+
def google_client
|
24
|
+
@google_client ||= begin
|
25
|
+
require "google/apis/cloudkms_v1"
|
26
|
+
client = ::Google::Apis::CloudkmsV1::CloudKMSService.new
|
27
|
+
client.authorization = ::Google::Auth.get_application_default(
|
28
|
+
"https://www.googleapis.com/auth/cloud-platform"
|
29
|
+
)
|
30
|
+
client
|
31
|
+
end
|
43
32
|
end
|
44
|
-
end
|
45
33
|
|
46
|
-
|
47
|
-
|
48
|
-
retry_limit: 2,
|
49
|
-
http_open_timeout: 2,
|
50
|
-
http_read_timeout: 2
|
51
|
-
}
|
52
|
-
|
53
|
-
module Google
|
54
|
-
class << self
|
55
|
-
attr_writer :kms_client
|
56
|
-
|
57
|
-
def kms_client
|
58
|
-
@kms_client ||= begin
|
59
|
-
client = ::Google::Apis::CloudkmsV1::CloudKMSService.new
|
60
|
-
client.authorization = ::Google::Auth.get_application_default(
|
61
|
-
"https://www.googleapis.com/auth/cloud-platform"
|
62
|
-
)
|
63
|
-
client
|
64
|
-
end
|
65
|
-
end
|
34
|
+
def vault_client
|
35
|
+
@vault_client ||= ::Vault
|
66
36
|
end
|
67
37
|
end
|
68
38
|
end
|
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: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-11-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -136,6 +136,20 @@ dependencies:
|
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: vault
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
139
153
|
description:
|
140
154
|
email:
|
141
155
|
- andrew@chartkick.com
|
@@ -152,6 +166,7 @@ files:
|
|
152
166
|
- Rakefile
|
153
167
|
- guides/Amazon.md
|
154
168
|
- guides/Google.md
|
169
|
+
- guides/Vault.md
|
155
170
|
- kms_encrypted.gemspec
|
156
171
|
- lib/kms_encrypted.rb
|
157
172
|
- lib/kms_encrypted/log_subscriber.rb
|
@@ -177,7 +192,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
177
192
|
version: '0'
|
178
193
|
requirements: []
|
179
194
|
rubyforge_project:
|
180
|
-
rubygems_version: 2.
|
195
|
+
rubygems_version: 2.7.7
|
181
196
|
signing_key:
|
182
197
|
specification_version: 4
|
183
198
|
summary: Simple, secure key management for attr_encrypted
|