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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1e8f9ae8d4c6e01a94c6daa2ae422b57ce4bcec1
4
- data.tar.gz: fe2ad3ac82398c643ae60c5a88830c6108864c1c
3
+ metadata.gz: 81d18dbb1dbaa5711a97eef2a4ece83fa44e2c2c
4
+ data.tar.gz: f7f8beede414a85f9ca399d8f499ff0f48b2bf53
5
5
  SHA512:
6
- metadata.gz: aee2523daea0e3cc176606d845d94ed98ba8ca8a2a51beb90227c638e7d0f84e888639846d78585d446f5dae7371b41c5e98b603b50330423232c0e1e5fe1976
7
- data.tar.gz: 841ec21de2384e5d062c6e3f69d3f4a12cabe8cb99f21f169d1bbb931191f21f9b42964dbb1d8f98015b349df931e41dfc0dcf958fc3cb736913918478de429b
6
+ metadata.gz: 3db72cc8666e461acedc3842829cc17e7ab0d32820eb9729e8fc1682dded9bed440f6d1201a28dacf5fdbcd350c6eaaa0035fe13274b37102a749cc8dd2c1a9f
7
+ data.tar.gz: 2964f2377c884927a6dc31ea1d48aa24d3e9ac6f4b70924da5fec5d05176bb98c715357f1d4032001b6f49485c63ddcf1550e4b5d271b05507e295bc6f52b9a4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.2.0
2
+
3
+ - Added support for Google KMS
4
+
1
5
  ## 0.1.4
2
6
 
3
7
  - Added `kms_keys` method to models
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
- [KMS](https://aws.amazon.com/kms/) addresses all of these issues and it’s easy to use them together.
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 [data key](http://docs.aws.amazon.com/kms/latest/developerguide/concepts.html) from our KMS master key. KMS sends both encrypted and unencrypted versions of the data key. 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.
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
- Add this line to your application’s Gemfile:
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
- ```ruby
277
- user.rotate_kms_key_phone!
278
- ```
30
+ - [Amazon KMS](guides/Amazon.md)
31
+ - [Google KMS](guides/Google.md)
279
32
 
280
33
  ## History
281
34
 
282
- View the [changelog](https://github.com/ankane/kms_encrypted/blob/master/CHANGELOG.md)
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
+ ```
@@ -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
- require "aws-sdk-kms"
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
@@ -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 key_id == "insecure-test-key"
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,
@@ -1,3 +1,3 @@
1
1
  module KmsEncrypted
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.4
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: 2017-12-04 00:00:00.000000000 Z
11
+ date: 2018-02-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: aws-sdk-kms
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: activesupport
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: :runtime
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: bundler
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: rake
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: minitest
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: sqlite3
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: activerecord
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: attr_encrypted
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: []