kms_encrypted 0.1.4 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +8 -255
- data/guides/Amazon.md +262 -0
- data/guides/Google.md +131 -0
- data/kms_encrypted.gemspec +3 -1
- data/lib/kms_encrypted.rb +34 -1
- data/lib/kms_encrypted/model.rb +38 -3
- data/lib/kms_encrypted/version.rb +1 -1
- metadata +30 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 81d18dbb1dbaa5711a97eef2a4ece83fa44e2c2c
|
4
|
+
data.tar.gz: f7f8beede414a85f9ca399d8f499ff0f48b2bf53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3db72cc8666e461acedc3842829cc17e7ab0d32820eb9729e8fc1682dded9bed440f6d1201a28dacf5fdbcd350c6eaaa0035fe13274b37102a749cc8dd2c1a9f
|
7
|
+
data.tar.gz: 2964f2377c884927a6dc31ea1d48aa24d3e9ac6f4b70924da5fec5d05176bb98c715357f1d4032001b6f49485c63ddcf1550e4b5d271b05507e295bc6f52b9a4
|
data/CHANGELOG.md
CHANGED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2017 Andrew Kane
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
@@ -9,7 +9,9 @@ The attr_encrypted gem is great for encryption, but:
|
|
9
9
|
3. Doesn’t have a great audit trail to see how data has been accessed
|
10
10
|
4. Doesn’t let you grant encryption and decryption permission separately
|
11
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/) and [Google KMS](https://cloud.google.com/kms/)
|
13
15
|
|
14
16
|
[![Build Status](https://travis-ci.org/ankane/kms_encrypted.svg?branch=master)](https://travis-ci.org/ankane/kms_encrypted)
|
15
17
|
|
@@ -17,269 +19,20 @@ The attr_encrypted gem is great for encryption, but:
|
|
17
19
|
|
18
20
|
This approach uses KMS to manage encryption keys and attr_encrypted to do the encryption.
|
19
21
|
|
20
|
-
To encrypt an attribute, we first generate a
|
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
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
25
|
|
24
26
|
## Getting Started
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
```ruby
|
29
|
-
gem 'kms_encrypted'
|
30
|
-
```
|
31
|
-
|
32
|
-
Add columns for the encrypted data and the encrypted KMS data keys
|
33
|
-
|
34
|
-
```ruby
|
35
|
-
add_column :users, :encrypted_email, :text
|
36
|
-
add_column :users, :encrypted_email_iv, :text
|
37
|
-
add_column :users, :encrypted_kms_key, :text
|
38
|
-
```
|
39
|
-
|
40
|
-
Create an [Amazon Web Services](https://aws.amazon.com/) account if you don’t have one. KMS works great whether or not you run your infrastructure on AWS.
|
41
|
-
|
42
|
-
Create a [KMS master key](https://console.aws.amazon.com/iam/home#/encryptionKeys) and set it in your environment ([dotenv](https://github.com/bkeepers/dotenv) is great for this)
|
43
|
-
|
44
|
-
```sh
|
45
|
-
KMS_KEY_ID=arn:aws:kms:...
|
46
|
-
```
|
47
|
-
|
48
|
-
You can also use the alias
|
49
|
-
|
50
|
-
```sh
|
51
|
-
KMS_KEY_ID=alias/my-alias
|
52
|
-
```
|
53
|
-
|
54
|
-
And update your model
|
55
|
-
|
56
|
-
```ruby
|
57
|
-
class User < ApplicationRecord
|
58
|
-
has_kms_key
|
59
|
-
|
60
|
-
attr_encrypted :email, key: :kms_key
|
61
|
-
end
|
62
|
-
```
|
63
|
-
|
64
|
-
For each encrypted attribute, use the `kms_key` method for its key.
|
65
|
-
|
66
|
-
## Auditing
|
67
|
-
|
68
|
-
[AWS CloudTrail](https://aws.amazon.com/cloudtrail/) logs all decryption calls. However, to know what data is being decrypted, you’ll need to add context.
|
69
|
-
|
70
|
-
Add a `kms_encryption_context` method to your model.
|
71
|
-
|
72
|
-
```ruby
|
73
|
-
class User < ApplicationRecord
|
74
|
-
def kms_encryption_context
|
75
|
-
# some hash
|
76
|
-
end
|
77
|
-
end
|
78
|
-
```
|
79
|
-
|
80
|
-
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](http://docs.aws.amazon.com/kms/latest/developerguide/encryption-context.html).
|
81
|
-
|
82
|
-
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:
|
83
|
-
|
84
|
-
```ruby
|
85
|
-
class User < ApplicationRecord
|
86
|
-
def kms_encryption_context
|
87
|
-
self.id ||= self.class.connection.execute("select nextval('#{self.class.sequence_name}')").first["nextval"]
|
88
|
-
{"Record" => "#{model_name}/#{id}"}
|
89
|
-
end
|
90
|
-
end
|
91
|
-
```
|
92
|
-
|
93
|
-
[Amazon Athena](https://aws.amazon.com/athena/) is great for querying CloudTrail logs. Create a table (thanks to [this post](http://www.1strategy.com/blog/2017/07/25/auditing-aws-activity-with-cloudtrail-and-athena/) for the table structure) with:
|
94
|
-
|
95
|
-
```sql
|
96
|
-
CREATE EXTERNAL TABLE cloudtrail_logs (
|
97
|
-
eventversion STRING,
|
98
|
-
userIdentity STRUCT<
|
99
|
-
type:STRING,
|
100
|
-
principalid:STRING,
|
101
|
-
arn:STRING,
|
102
|
-
accountid:STRING,
|
103
|
-
invokedby:STRING,
|
104
|
-
accesskeyid:STRING,
|
105
|
-
userName:String,
|
106
|
-
sessioncontext:STRUCT<
|
107
|
-
attributes:STRUCT<
|
108
|
-
mfaauthenticated:STRING,
|
109
|
-
creationdate:STRING>,
|
110
|
-
sessionIssuer:STRUCT<
|
111
|
-
type:STRING,
|
112
|
-
principalId:STRING,
|
113
|
-
arn:STRING,
|
114
|
-
accountId:STRING,
|
115
|
-
userName:STRING>>>,
|
116
|
-
eventTime STRING,
|
117
|
-
eventSource STRING,
|
118
|
-
eventName STRING,
|
119
|
-
awsRegion STRING,
|
120
|
-
sourceIpAddress STRING,
|
121
|
-
userAgent STRING,
|
122
|
-
errorCode STRING,
|
123
|
-
errorMessage STRING,
|
124
|
-
requestId STRING,
|
125
|
-
eventId STRING,
|
126
|
-
resources ARRAY<STRUCT<
|
127
|
-
ARN:STRING,
|
128
|
-
accountId:STRING,
|
129
|
-
type:STRING>>,
|
130
|
-
eventType STRING,
|
131
|
-
apiVersion STRING,
|
132
|
-
readOnly BOOLEAN,
|
133
|
-
recipientAccountId STRING,
|
134
|
-
sharedEventID STRING,
|
135
|
-
vpcEndpointId STRING,
|
136
|
-
requestParameters STRING,
|
137
|
-
responseElements STRING,
|
138
|
-
additionalEventData STRING,
|
139
|
-
serviceEventDetails STRING
|
140
|
-
)
|
141
|
-
ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde'
|
142
|
-
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
|
143
|
-
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
|
144
|
-
LOCATION 's3://my-cloudtrail-logs/'
|
145
|
-
```
|
146
|
-
|
147
|
-
Change the last line to point to your CloudTrail log bucket and query away
|
148
|
-
|
149
|
-
```sql
|
150
|
-
SELECT
|
151
|
-
eventTime,
|
152
|
-
userIdentity.userName,
|
153
|
-
requestParameters
|
154
|
-
FROM
|
155
|
-
cloudtrail_logs
|
156
|
-
WHERE
|
157
|
-
eventName = 'Decrypt'
|
158
|
-
AND resources[1].arn = 'arn:aws:kms:...'
|
159
|
-
ORDER BY 1
|
160
|
-
```
|
161
|
-
|
162
|
-
There will also be `GenerateDataKey` events.
|
163
|
-
|
164
|
-
## Alerting
|
165
|
-
|
166
|
-
We recommend setting up alerts on suspicious behavior.
|
167
|
-
|
168
|
-
## Key Rotation
|
169
|
-
|
170
|
-
KMS supports [automatic key rotation](http://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html). No action is required in this case.
|
171
|
-
|
172
|
-
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).
|
173
|
-
|
174
|
-
```sh
|
175
|
-
KMS_KEY_ID=arn:aws:kms:...
|
176
|
-
```
|
177
|
-
|
178
|
-
and run
|
179
|
-
|
180
|
-
```ruby
|
181
|
-
User.find_each do |user|
|
182
|
-
user.rotate_kms_key!
|
183
|
-
end
|
184
|
-
```
|
185
|
-
|
186
|
-
## IAM Permissions
|
187
|
-
|
188
|
-
A great feature of KMS is the ability to grant encryption and decryption permission separately.
|
189
|
-
|
190
|
-
To encrypt the data, use a policy with:
|
191
|
-
|
192
|
-
```json
|
193
|
-
{
|
194
|
-
"Version": "2012-10-17",
|
195
|
-
"Statement": [
|
196
|
-
{
|
197
|
-
"Sid": "EncryptData",
|
198
|
-
"Effect": "Allow",
|
199
|
-
"Action": "kms:GenerateDataKey",
|
200
|
-
"Resource": "arn:aws:kms:..."
|
201
|
-
}
|
202
|
-
]
|
203
|
-
}
|
204
|
-
```
|
205
|
-
|
206
|
-
If a system can only encrypt, you must clear out existing data and data keys before updates.
|
207
|
-
|
208
|
-
```ruby
|
209
|
-
user.encrypted_email = nil
|
210
|
-
user.encrypted_kms_key = nil
|
211
|
-
# before user.save or user.update
|
212
|
-
```
|
213
|
-
|
214
|
-
To decrypt the data, use a policy with:
|
215
|
-
|
216
|
-
```json
|
217
|
-
{
|
218
|
-
"Version": "2012-10-17",
|
219
|
-
"Statement": [
|
220
|
-
{
|
221
|
-
"Sid": "DecryptData",
|
222
|
-
"Effect": "Allow",
|
223
|
-
"Action": "kms:Decrypt",
|
224
|
-
"Resource": "arn:aws:kms:..."
|
225
|
-
}
|
226
|
-
]
|
227
|
-
}
|
228
|
-
```
|
229
|
-
|
230
|
-
Be extremely selective of systems you allow to decrypt.
|
231
|
-
|
232
|
-
## Testing
|
233
|
-
|
234
|
-
For testing, you can prevent network calls to KMS by setting:
|
235
|
-
|
236
|
-
```sh
|
237
|
-
KMS_KEY_ID=insecure-test-key
|
238
|
-
```
|
239
|
-
|
240
|
-
## Multiple Keys Per Record
|
241
|
-
|
242
|
-
You may want to protect different columns with different data keys (or even master keys).
|
243
|
-
|
244
|
-
To do this, add more columns
|
245
|
-
|
246
|
-
```ruby
|
247
|
-
add_column :users, :encrypted_phone, :text
|
248
|
-
add_column :users, :encrypted_phone_iv, :text
|
249
|
-
add_column :users, :encrypted_kms_key_phone, :text
|
250
|
-
```
|
251
|
-
|
252
|
-
And update your model
|
253
|
-
|
254
|
-
```ruby
|
255
|
-
class User < ApplicationRecord
|
256
|
-
has_kms_key
|
257
|
-
has_kms_key name: :phone, key_id: "..."
|
258
|
-
|
259
|
-
attr_encrypted :email, key: :kms_key
|
260
|
-
attr_encrypted :phone, key: :kms_key_phone
|
261
|
-
end
|
262
|
-
```
|
263
|
-
|
264
|
-
For context, use:
|
265
|
-
|
266
|
-
```ruby
|
267
|
-
class User < ApplicationRecord
|
268
|
-
def kms_encryption_context_phone
|
269
|
-
# some hash
|
270
|
-
end
|
271
|
-
end
|
272
|
-
```
|
273
|
-
|
274
|
-
To rotate keys, use:
|
28
|
+
Follow the instructions for your key management service:
|
275
29
|
|
276
|
-
|
277
|
-
|
278
|
-
```
|
30
|
+
- [Amazon KMS](guides/Amazon.md)
|
31
|
+
- [Google KMS](guides/Google.md)
|
279
32
|
|
280
33
|
## History
|
281
34
|
|
282
|
-
View the [changelog](
|
35
|
+
View the [changelog](CHANGELOG.md)
|
283
36
|
|
284
37
|
## Contributing
|
285
38
|
|
data/guides/Amazon.md
ADDED
@@ -0,0 +1,262 @@
|
|
1
|
+
# Amazon KMS
|
2
|
+
|
3
|
+
Add this line to your application’s Gemfile:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
gem 'aws-sdk-kms'
|
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
|
+
Create an [Amazon Web Services](https://aws.amazon.com/) account if you don’t have one. KMS works great whether or not you run your infrastructure on AWS.
|
19
|
+
|
20
|
+
Create a [KMS master key](https://console.aws.amazon.com/iam/home#/encryptionKeys) and set it in your environment along with your AWS credentials ([dotenv](https://github.com/bkeepers/dotenv) is great for this)
|
21
|
+
|
22
|
+
```sh
|
23
|
+
KMS_KEY_ID=arn:aws:kms:...
|
24
|
+
AWS_ACCESS_KEY_ID=...
|
25
|
+
AWS_SECRET_ACCESS_KEY=...
|
26
|
+
```
|
27
|
+
|
28
|
+
You can also use the alias
|
29
|
+
|
30
|
+
```sh
|
31
|
+
KMS_KEY_ID=alias/my-alias
|
32
|
+
```
|
33
|
+
|
34
|
+
And update your model
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class User < ApplicationRecord
|
38
|
+
has_kms_key
|
39
|
+
|
40
|
+
attr_encrypted :email, key: :kms_key
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
For each encrypted attribute, use the `kms_key` method for its key.
|
45
|
+
|
46
|
+
## Auditing
|
47
|
+
|
48
|
+
[AWS CloudTrail](https://aws.amazon.com/cloudtrail/) logs all decryption calls. However, to know what data is being decrypted, you’ll need to add context.
|
49
|
+
|
50
|
+
Add a `kms_encryption_context` method to your model.
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class User < ApplicationRecord
|
54
|
+
def kms_encryption_context
|
55
|
+
# some hash
|
56
|
+
end
|
57
|
+
end
|
58
|
+
```
|
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](http://docs.aws.amazon.com/kms/latest/developerguide/encryption-context.html).
|
61
|
+
|
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
|
+
|
64
|
+
```ruby
|
65
|
+
class User < ApplicationRecord
|
66
|
+
def kms_encryption_context
|
67
|
+
self.id ||= self.class.connection.execute("select nextval('#{self.class.sequence_name}')").first["nextval"]
|
68
|
+
{"Record" => "#{model_name}/#{id}"}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
[Amazon Athena](https://aws.amazon.com/athena/) is great for querying CloudTrail logs. Create a table (thanks to [this post](http://www.1strategy.com/blog/2017/07/25/auditing-aws-activity-with-cloudtrail-and-athena/) for the table structure) with:
|
74
|
+
|
75
|
+
```sql
|
76
|
+
CREATE EXTERNAL TABLE cloudtrail_logs (
|
77
|
+
eventversion STRING,
|
78
|
+
userIdentity STRUCT<
|
79
|
+
type:STRING,
|
80
|
+
principalid:STRING,
|
81
|
+
arn:STRING,
|
82
|
+
accountid:STRING,
|
83
|
+
invokedby:STRING,
|
84
|
+
accesskeyid:STRING,
|
85
|
+
userName:String,
|
86
|
+
sessioncontext:STRUCT<
|
87
|
+
attributes:STRUCT<
|
88
|
+
mfaauthenticated:STRING,
|
89
|
+
creationdate:STRING>,
|
90
|
+
sessionIssuer:STRUCT<
|
91
|
+
type:STRING,
|
92
|
+
principalId:STRING,
|
93
|
+
arn:STRING,
|
94
|
+
accountId:STRING,
|
95
|
+
userName:STRING>>>,
|
96
|
+
eventTime STRING,
|
97
|
+
eventSource STRING,
|
98
|
+
eventName STRING,
|
99
|
+
awsRegion STRING,
|
100
|
+
sourceIpAddress STRING,
|
101
|
+
userAgent STRING,
|
102
|
+
errorCode STRING,
|
103
|
+
errorMessage STRING,
|
104
|
+
requestId STRING,
|
105
|
+
eventId STRING,
|
106
|
+
resources ARRAY<STRUCT<
|
107
|
+
ARN:STRING,
|
108
|
+
accountId:STRING,
|
109
|
+
type:STRING>>,
|
110
|
+
eventType STRING,
|
111
|
+
apiVersion STRING,
|
112
|
+
readOnly BOOLEAN,
|
113
|
+
recipientAccountId STRING,
|
114
|
+
sharedEventID STRING,
|
115
|
+
vpcEndpointId STRING,
|
116
|
+
requestParameters STRING,
|
117
|
+
responseElements STRING,
|
118
|
+
additionalEventData STRING,
|
119
|
+
serviceEventDetails STRING
|
120
|
+
)
|
121
|
+
ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde'
|
122
|
+
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
|
123
|
+
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
|
124
|
+
LOCATION 's3://my-cloudtrail-logs/'
|
125
|
+
```
|
126
|
+
|
127
|
+
Change the last line to point to your CloudTrail log bucket and query away
|
128
|
+
|
129
|
+
```sql
|
130
|
+
SELECT
|
131
|
+
eventTime,
|
132
|
+
userIdentity.userName,
|
133
|
+
requestParameters
|
134
|
+
FROM
|
135
|
+
cloudtrail_logs
|
136
|
+
WHERE
|
137
|
+
eventName = 'Decrypt'
|
138
|
+
AND resources[1].arn = 'arn:aws:kms:...'
|
139
|
+
ORDER BY 1
|
140
|
+
```
|
141
|
+
|
142
|
+
There will also be `GenerateDataKey` events.
|
143
|
+
|
144
|
+
## Alerting
|
145
|
+
|
146
|
+
We recommend setting up alerts on suspicious behavior.
|
147
|
+
|
148
|
+
## Key Rotation
|
149
|
+
|
150
|
+
KMS supports [automatic key rotation](http://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html). No action is required in this case.
|
151
|
+
|
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
|
+
|
154
|
+
```sh
|
155
|
+
KMS_KEY_ID=arn:aws:kms:...
|
156
|
+
```
|
157
|
+
|
158
|
+
and run
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
User.find_each do |user|
|
162
|
+
user.rotate_kms_key!
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
## IAM Permissions
|
167
|
+
|
168
|
+
A great feature of KMS is the ability to grant encryption and decryption permission separately.
|
169
|
+
|
170
|
+
To encrypt the data, use a policy with:
|
171
|
+
|
172
|
+
```json
|
173
|
+
{
|
174
|
+
"Version": "2012-10-17",
|
175
|
+
"Statement": [
|
176
|
+
{
|
177
|
+
"Sid": "EncryptData",
|
178
|
+
"Effect": "Allow",
|
179
|
+
"Action": "kms:GenerateDataKey",
|
180
|
+
"Resource": "arn:aws:kms:..."
|
181
|
+
}
|
182
|
+
]
|
183
|
+
}
|
184
|
+
```
|
185
|
+
|
186
|
+
If a system can only encrypt, you must clear out existing data and data keys before updates.
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
user.encrypted_email = nil
|
190
|
+
user.encrypted_kms_key = nil
|
191
|
+
# before user.save or user.update
|
192
|
+
```
|
193
|
+
|
194
|
+
To decrypt the data, use a policy with:
|
195
|
+
|
196
|
+
```json
|
197
|
+
{
|
198
|
+
"Version": "2012-10-17",
|
199
|
+
"Statement": [
|
200
|
+
{
|
201
|
+
"Sid": "DecryptData",
|
202
|
+
"Effect": "Allow",
|
203
|
+
"Action": "kms:Decrypt",
|
204
|
+
"Resource": "arn:aws:kms:..."
|
205
|
+
}
|
206
|
+
]
|
207
|
+
}
|
208
|
+
```
|
209
|
+
|
210
|
+
Be extremely selective of systems you allow to decrypt.
|
211
|
+
|
212
|
+
## Testing
|
213
|
+
|
214
|
+
For testing, you can prevent network calls to KMS by setting:
|
215
|
+
|
216
|
+
```sh
|
217
|
+
KMS_KEY_ID=insecure-test-key
|
218
|
+
```
|
219
|
+
|
220
|
+
## Multiple Keys Per Record
|
221
|
+
|
222
|
+
You may want to protect different columns with different data keys (or even master keys).
|
223
|
+
|
224
|
+
To do this, add more columns
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
add_column :users, :encrypted_phone, :text
|
228
|
+
add_column :users, :encrypted_phone_iv, :text
|
229
|
+
add_column :users, :encrypted_kms_key_phone, :text
|
230
|
+
```
|
231
|
+
|
232
|
+
And update your model
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
class User < ApplicationRecord
|
236
|
+
has_kms_key
|
237
|
+
has_kms_key name: :phone, key_id: "..."
|
238
|
+
|
239
|
+
attr_encrypted :email, key: :kms_key
|
240
|
+
attr_encrypted :phone, key: :kms_key_phone
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
For context, use:
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
class User < ApplicationRecord
|
248
|
+
def kms_encryption_context_phone
|
249
|
+
# some hash
|
250
|
+
end
|
251
|
+
end
|
252
|
+
```
|
253
|
+
|
254
|
+
To rotate keys, use:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
user.rotate_kms_key_phone!
|
258
|
+
```
|
259
|
+
|
260
|
+
## File Uploads
|
261
|
+
|
262
|
+
While outside the scope of this gem, you can also use KMS for sensitive file uploads. Check out [this guide](https://github.com/ankane/shorts/blob/master/AWS-Client-Side-Encryption.md) to learn more.
|
data/guides/Google.md
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
# Google KMS
|
2
|
+
|
3
|
+
Add this line to your application’s Gemfile:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
gem 'google-api-client'
|
7
|
+
gem 'kms_encrypted', github: 'ankane/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
|
+
Create a [Google Cloud Platform](https://cloud.google.com/) account if you don’t have one. KMS works great whether or not you run your infrastructure on GCP.
|
19
|
+
|
20
|
+
Create a [KMS key ring and key](https://console.cloud.google.com/iam-admin/kms) and set it in your environment along with your GCP credentials ([dotenv](https://github.com/bkeepers/dotenv) is great for this)
|
21
|
+
|
22
|
+
```sh
|
23
|
+
KMS_KEY_ID=projects/.../locations/.../keyRings/.../cryptoKeys/...
|
24
|
+
```
|
25
|
+
|
26
|
+
And update your model
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class User < ApplicationRecord
|
30
|
+
has_kms_key
|
31
|
+
|
32
|
+
attr_encrypted :email, key: :kms_key
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
For each encrypted attribute, use the `kms_key` method for its key.
|
37
|
+
|
38
|
+
## Auditing
|
39
|
+
|
40
|
+
Follow the [instructions here](https://cloud.google.com/kms/docs/logging) to set up data access logging. To know what data is being decrypted, you’ll need to add context.
|
41
|
+
|
42
|
+
Add a `kms_encryption_context` method to your model.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class User < ApplicationRecord
|
46
|
+
def kms_encryption_context
|
47
|
+
# some hash
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
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.
|
53
|
+
|
54
|
+
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:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class User < ApplicationRecord
|
58
|
+
def kms_encryption_context
|
59
|
+
self.id ||= self.class.connection.execute("select nextval('#{self.class.sequence_name}')").first["nextval"]
|
60
|
+
{"Record" => "#{model_name}/#{id}"}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
## Alerting
|
66
|
+
|
67
|
+
We recommend setting up alerts on suspicious behavior.
|
68
|
+
|
69
|
+
## Key Rotation
|
70
|
+
|
71
|
+
To manually rotate keys, replace the old 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 GCP account).
|
72
|
+
|
73
|
+
```sh
|
74
|
+
KMS_KEY_ID=...
|
75
|
+
```
|
76
|
+
|
77
|
+
and run
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
User.find_each do |user|
|
81
|
+
user.rotate_kms_key!
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
## Testing
|
86
|
+
|
87
|
+
For testing, you can prevent network calls to KMS by setting:
|
88
|
+
|
89
|
+
```sh
|
90
|
+
KMS_KEY_ID=insecure-test-key
|
91
|
+
```
|
92
|
+
|
93
|
+
## Multiple Keys Per Record
|
94
|
+
|
95
|
+
You may want to protect different columns with different data keys (or even master keys).
|
96
|
+
|
97
|
+
To do this, add more columns
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
add_column :users, :encrypted_phone, :text
|
101
|
+
add_column :users, :encrypted_phone_iv, :text
|
102
|
+
add_column :users, :encrypted_kms_key_phone, :text
|
103
|
+
```
|
104
|
+
|
105
|
+
And update your model
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
class User < ApplicationRecord
|
109
|
+
has_kms_key
|
110
|
+
has_kms_key name: :phone, key_id: "..."
|
111
|
+
|
112
|
+
attr_encrypted :email, key: :kms_key
|
113
|
+
attr_encrypted :phone, key: :kms_key_phone
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
For context, use:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class User < ApplicationRecord
|
121
|
+
def kms_encryption_context_phone
|
122
|
+
# some hash
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
To rotate keys, use:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
user.rotate_kms_key_phone!
|
131
|
+
```
|
data/kms_encrypted.gemspec
CHANGED
@@ -11,6 +11,7 @@ Gem::Specification.new do |spec|
|
|
11
11
|
|
12
12
|
spec.summary = "Simple, secure key management for attr_encrypted"
|
13
13
|
spec.homepage = "https://github.com/ankane/kms_encrypted"
|
14
|
+
spec.license = "MIT"
|
14
15
|
|
15
16
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
17
|
f.match(%r{^(test|spec|features)/})
|
@@ -19,7 +20,6 @@ Gem::Specification.new do |spec|
|
|
19
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
21
|
spec.require_paths = ["lib"]
|
21
22
|
|
22
|
-
spec.add_dependency "aws-sdk-kms"
|
23
23
|
spec.add_dependency "activesupport"
|
24
24
|
|
25
25
|
spec.add_development_dependency "bundler"
|
@@ -28,4 +28,6 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.add_development_dependency "sqlite3"
|
29
29
|
spec.add_development_dependency "activerecord"
|
30
30
|
spec.add_development_dependency "attr_encrypted"
|
31
|
+
spec.add_development_dependency "aws-sdk-kms"
|
32
|
+
spec.add_development_dependency "google-api-client"
|
31
33
|
end
|
data/lib/kms_encrypted.rb
CHANGED
@@ -1,6 +1,23 @@
|
|
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
|
4
21
|
|
5
22
|
# modules
|
6
23
|
require "kms_encrypted/log_subscriber"
|
@@ -32,6 +49,22 @@ module KmsEncrypted
|
|
32
49
|
http_open_timeout: 2,
|
33
50
|
http_read_timeout: 2
|
34
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
|
66
|
+
end
|
67
|
+
end
|
35
68
|
end
|
36
69
|
|
37
70
|
ActiveSupport.on_load(:active_record) do
|
data/lib/kms_encrypted/model.rb
CHANGED
@@ -46,16 +46,35 @@ module KmsEncrypted
|
|
46
46
|
}
|
47
47
|
ActiveSupport::Notifications.instrument("generate_data_key.kms_encrypted", event) do
|
48
48
|
if key_id == "insecure-test-key"
|
49
|
-
encrypted_key = "insecure-data-key-#{rand(1_000_000_000_000)}"
|
50
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
|
+
request = ::Google::Apis::CloudkmsV1::EncryptRequest.new(
|
57
|
+
plaintext: plaintext_key,
|
58
|
+
additional_authenticated_data: context.to_json
|
59
|
+
)
|
60
|
+
response = KmsEncrypted::Google.kms_client.encrypt_crypto_key(key_id, request)
|
61
|
+
key_version = response.name
|
62
|
+
|
63
|
+
# shorten key to save space
|
64
|
+
short_key_id = Base64.encode64(key_version.split("/").select.with_index { |p, i| i.odd? }.join("/"))
|
65
|
+
|
66
|
+
# build encrypted key
|
67
|
+
# we reference the key in the field for easy rotation
|
68
|
+
encrypted_key = "$gc$#{short_key_id}$#{[response.ciphertext].pack(default_encoding)}"
|
51
69
|
else
|
70
|
+
# generate data key from API
|
52
71
|
resp = KmsEncrypted.kms_client.generate_data_key(
|
53
72
|
key_id: key_id,
|
54
73
|
encryption_context: context,
|
55
74
|
key_spec: "AES_256"
|
56
75
|
)
|
57
|
-
encrypted_key = [resp.ciphertext_blob].pack(default_encoding)
|
58
76
|
plaintext_key = resp.plaintext
|
77
|
+
encrypted_key = [resp.ciphertext_blob].pack(default_encoding)
|
59
78
|
end
|
60
79
|
end
|
61
80
|
|
@@ -72,8 +91,24 @@ module KmsEncrypted
|
|
72
91
|
context: context
|
73
92
|
}
|
74
93
|
ActiveSupport::Notifications.instrument("decrypt_data_key.kms_encrypted", event) do
|
75
|
-
if
|
94
|
+
if encrypted_key.start_with?("insecure-data-key-")
|
76
95
|
plaintext_key = "00000000000000000000000000000000"
|
96
|
+
elsif encrypted_key.start_with?("$gc$")
|
97
|
+
_, _, short_key_id, ciphertext = encrypted_key.split("$", 4)
|
98
|
+
|
99
|
+
# restore key, except for cryptoKeyVersion
|
100
|
+
stored_key_id = Base64.decode64(short_key_id).split("/")[0..3]
|
101
|
+
stored_key_id.insert(0, "projects")
|
102
|
+
stored_key_id.insert(2, "locations")
|
103
|
+
stored_key_id.insert(4, "keyRings")
|
104
|
+
stored_key_id.insert(6, "cryptoKeys")
|
105
|
+
stored_key_id = stored_key_id.join("/")
|
106
|
+
|
107
|
+
request = ::Google::Apis::CloudkmsV1::DecryptRequest.new(
|
108
|
+
ciphertext: ciphertext.unpack(default_encoding).first,
|
109
|
+
additional_authenticated_data: context.to_json
|
110
|
+
)
|
111
|
+
plaintext_key = KmsEncrypted::Google.kms_client.decrypt_crypto_key(stored_key_id, request).plaintext
|
77
112
|
else
|
78
113
|
plaintext_key = KmsEncrypted.kms_client.decrypt(
|
79
114
|
ciphertext_blob: encrypted_key.unpack(default_encoding).first,
|
metadata
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kms_encrypted
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.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:
|
11
|
+
date: 2018-02-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: activesupport
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
@@ -25,13 +25,13 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
|
-
type: :
|
34
|
+
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
@@ -39,7 +39,7 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
@@ -53,7 +53,7 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: minitest
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - ">="
|
@@ -67,7 +67,7 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: sqlite3
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ">="
|
@@ -81,7 +81,7 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: activerecord
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - ">="
|
@@ -95,7 +95,7 @@ dependencies:
|
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: attr_encrypted
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - ">="
|
@@ -109,7 +109,21 @@ dependencies:
|
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
112
|
+
name: aws-sdk-kms
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: google-api-client
|
113
127
|
requirement: !ruby/object:Gem::Requirement
|
114
128
|
requirements:
|
115
129
|
- - ">="
|
@@ -133,15 +147,19 @@ files:
|
|
133
147
|
- ".travis.yml"
|
134
148
|
- CHANGELOG.md
|
135
149
|
- Gemfile
|
150
|
+
- LICENSE.txt
|
136
151
|
- README.md
|
137
152
|
- Rakefile
|
153
|
+
- guides/Amazon.md
|
154
|
+
- guides/Google.md
|
138
155
|
- kms_encrypted.gemspec
|
139
156
|
- lib/kms_encrypted.rb
|
140
157
|
- lib/kms_encrypted/log_subscriber.rb
|
141
158
|
- lib/kms_encrypted/model.rb
|
142
159
|
- lib/kms_encrypted/version.rb
|
143
160
|
homepage: https://github.com/ankane/kms_encrypted
|
144
|
-
licenses:
|
161
|
+
licenses:
|
162
|
+
- MIT
|
145
163
|
metadata: {}
|
146
164
|
post_install_message:
|
147
165
|
rdoc_options: []
|