kms_encrypted 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +140 -7
- data/lib/kms_encrypted/version.rb +1 -1
- data/lib/kms_encrypted.rb +36 -16
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 844c888ff1036e25341d665534193770b363f2d1
|
4
|
+
data.tar.gz: 042e30c929e4db3e7d7c18d692cc78ce220932c7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 14e1be577cf7639c7f4eea48f1b03a56315adcd82d6efc4d38ecf6662793947202c4ded1f0d674eb860b60da4e8081a65515949a3810384c6c7b91d666d8bc9a
|
7
|
+
data.tar.gz: 359927d6b27361c62434e49be3b1a90d9498119d73a5f098f903eadbad4afef3a879bb61ee19d8f8d41753a91e11cbe461a4b79a307d6a4b06238a5908a9cbea
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -7,10 +7,18 @@ The attr_encrypted gem is great for encryption, but:
|
|
7
7
|
1. Leaves you to manage the security of your keys
|
8
8
|
2. Doesn’t provide a great audit trail to see how data has been accessed
|
9
9
|
|
10
|
-
KMS addresses both
|
10
|
+
KMS addresses both issues and it’s easy to use them together.
|
11
11
|
|
12
12
|
**Note:** This has not been battle-tested in a production environment, so use with caution
|
13
13
|
|
14
|
+
## How It Works
|
15
|
+
|
16
|
+
This approach uses KMS to manage encryption keys and attr_encrypted to do the encryption.
|
17
|
+
|
18
|
+
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.
|
19
|
+
|
20
|
+
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.
|
21
|
+
|
14
22
|
## Getting Started
|
15
23
|
|
16
24
|
Add this line to your application’s Gemfile:
|
@@ -49,6 +57,18 @@ For each encrypted attribute, use the `kms_key` method for its key.
|
|
49
57
|
|
50
58
|
Add a `kms_encryption_context` method to your model.
|
51
59
|
|
60
|
+
```ruby
|
61
|
+
class User < ApplicationRecord
|
62
|
+
def kms_encryption_context
|
63
|
+
# some hash
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
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).
|
69
|
+
|
70
|
+
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:
|
71
|
+
|
52
72
|
```ruby
|
53
73
|
class User < ApplicationRecord
|
54
74
|
def kms_encryption_context
|
@@ -58,17 +78,130 @@ class User < ApplicationRecord
|
|
58
78
|
end
|
59
79
|
```
|
60
80
|
|
61
|
-
|
81
|
+
We recommend [Amazon Athena](https://aws.amazon.com/athena/) 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:
|
82
|
+
|
83
|
+
```sql
|
84
|
+
CREATE EXTERNAL TABLE cloudtrail_logs (
|
85
|
+
eventversion STRING,
|
86
|
+
userIdentity STRUCT<
|
87
|
+
type:STRING,
|
88
|
+
principalid:STRING,
|
89
|
+
arn:STRING,
|
90
|
+
accountid:STRING,
|
91
|
+
invokedby:STRING,
|
92
|
+
accesskeyid:STRING,
|
93
|
+
userName:String,
|
94
|
+
sessioncontext:STRUCT<
|
95
|
+
attributes:STRUCT<
|
96
|
+
mfaauthenticated:STRING,
|
97
|
+
creationdate:STRING>,
|
98
|
+
sessionIssuer:STRUCT<
|
99
|
+
type:STRING,
|
100
|
+
principalId:STRING,
|
101
|
+
arn:STRING,
|
102
|
+
accountId:STRING,
|
103
|
+
userName:STRING>>>,
|
104
|
+
eventTime STRING,
|
105
|
+
eventSource STRING,
|
106
|
+
eventName STRING,
|
107
|
+
awsRegion STRING,
|
108
|
+
sourceIpAddress STRING,
|
109
|
+
userAgent STRING,
|
110
|
+
errorCode STRING,
|
111
|
+
errorMessage STRING,
|
112
|
+
requestId STRING,
|
113
|
+
eventId STRING,
|
114
|
+
resources ARRAY<STRUCT<
|
115
|
+
ARN:STRING,
|
116
|
+
accountId:STRING,
|
117
|
+
type:STRING>>,
|
118
|
+
eventType STRING,
|
119
|
+
apiVersion STRING,
|
120
|
+
readOnly BOOLEAN,
|
121
|
+
recipientAccountId STRING,
|
122
|
+
sharedEventID STRING,
|
123
|
+
vpcEndpointId STRING,
|
124
|
+
requestParameters STRING,
|
125
|
+
responseElements STRING,
|
126
|
+
additionalEventData STRING,
|
127
|
+
serviceEventDetails STRING
|
128
|
+
)
|
129
|
+
ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde'
|
130
|
+
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
|
131
|
+
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
|
132
|
+
LOCATION 's3://my-cloudtrail-logs/'
|
133
|
+
```
|
62
134
|
|
63
|
-
|
135
|
+
Change the last line to point to your CloudTrail log bucket and query away
|
136
|
+
|
137
|
+
```sql
|
138
|
+
SELECT
|
139
|
+
eventTime,
|
140
|
+
eventName,
|
141
|
+
userIdentity.userName,
|
142
|
+
requestParameters
|
143
|
+
FROM
|
144
|
+
cloudtrail_logs
|
145
|
+
WHERE
|
146
|
+
eventName = 'Decrypt'
|
147
|
+
ORDER BY 1
|
148
|
+
```
|
64
149
|
|
65
|
-
##
|
150
|
+
## Key Rotation
|
151
|
+
|
152
|
+
KMS supports [automatic key rotation](http://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html). No action is required in this case.
|
153
|
+
|
154
|
+
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).
|
155
|
+
|
156
|
+
```sh
|
157
|
+
KMS_KEY_ID=arn:aws:kms:...
|
158
|
+
```
|
159
|
+
|
160
|
+
and run
|
161
|
+
|
162
|
+
```sh
|
163
|
+
User.find_each do |user|
|
164
|
+
user.rotate_kms_key!
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
## Multiple Keys Per Record
|
169
|
+
|
170
|
+
You may want to protect different columns with different data keys (or even master keys).
|
171
|
+
|
172
|
+
To do this, add more columns
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
add_column :users, :encrypted_kms_key_phone, :string
|
176
|
+
```
|
177
|
+
|
178
|
+
And update your model
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
class User < ApplicationRecord
|
182
|
+
has_kms_key ENV["KMS_KEY_ID"]
|
183
|
+
has_kms_key ENV["KMS_KEY_ID"], name: :phone
|
66
184
|
|
67
|
-
|
185
|
+
attr_encrypted :email, key: :kms_key
|
186
|
+
attr_encrypted :phone, key: :kms_key_phone
|
187
|
+
end
|
188
|
+
```
|
68
189
|
|
69
|
-
|
190
|
+
For context, use:
|
70
191
|
|
71
|
-
|
192
|
+
```ruby
|
193
|
+
class User < ApplicationRecord
|
194
|
+
def kms_encryption_context_phone
|
195
|
+
# some hash
|
196
|
+
end
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
200
|
+
To rotate keys, use:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
user.rotate_kms_key_phone!
|
204
|
+
```
|
72
205
|
|
73
206
|
## History
|
74
207
|
|
data/lib/kms_encrypted.rb
CHANGED
@@ -8,43 +8,63 @@ module KmsEncrypted
|
|
8
8
|
end
|
9
9
|
|
10
10
|
module Model
|
11
|
-
def has_kms_key(key_id)
|
11
|
+
def has_kms_key(key_id, name: nil)
|
12
12
|
raise ArgumentError, "Missing key id" unless key_id
|
13
13
|
|
14
|
+
key_method = name ? "kms_key_#{name}" : "kms_key"
|
15
|
+
|
14
16
|
class_eval do
|
15
|
-
|
16
|
-
|
17
|
-
end
|
18
|
-
self.kms_key_id = key_id
|
17
|
+
define_method(key_method) do
|
18
|
+
instance_var = "@#{key_method}"
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
context = respond_to?(
|
20
|
+
unless instance_variable_get(instance_var)
|
21
|
+
key_column = "encrypted_#{key_method}"
|
22
|
+
context_method = name ? "kms_encryption_context_#{name}" : "kms_encryption_context"
|
23
|
+
context = respond_to?(context_method) ? send(context_method) : {}
|
24
24
|
default_encoding = "m"
|
25
25
|
|
26
|
-
unless
|
26
|
+
unless send(key_column)
|
27
27
|
resp = KmsEncrypted.kms.generate_data_key(
|
28
28
|
key_id: key_id,
|
29
29
|
encryption_context: context,
|
30
30
|
key_spec: "AES_256"
|
31
31
|
)
|
32
|
-
@kms_key = resp.plaintext
|
33
32
|
ciphertext = resp.ciphertext_blob
|
34
|
-
|
33
|
+
instance_variable_set(instance_var, resp.plaintext)
|
34
|
+
self.send("#{key_column}=", [resp.ciphertext_blob].pack(default_encoding))
|
35
35
|
end
|
36
36
|
|
37
|
-
unless
|
38
|
-
ciphertext =
|
37
|
+
unless instance_variable_get(instance_var)
|
38
|
+
ciphertext = send(key_column).unpack(default_encoding).first
|
39
39
|
resp = KmsEncrypted.kms.decrypt(
|
40
40
|
ciphertext_blob: ciphertext,
|
41
41
|
encryption_context: context
|
42
42
|
)
|
43
|
-
|
43
|
+
instance_variable_set(instance_var, resp.plaintext)
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
47
|
-
|
47
|
+
instance_variable_get(instance_var)
|
48
|
+
end
|
49
|
+
|
50
|
+
define_method("rotate_#{key_method}!") do
|
51
|
+
# decrypt
|
52
|
+
plaintext_attributes = {}
|
53
|
+
self.class.encrypted_attributes.select { |_, v| v[:key] == key_method.to_sym }.keys.each do |key|
|
54
|
+
plaintext_attributes[key] = send(key)
|
55
|
+
end
|
56
|
+
|
57
|
+
# reset key
|
58
|
+
instance_variable_set("@#{key_method}", nil)
|
59
|
+
send("encrypted_#{key_method}=", nil)
|
60
|
+
|
61
|
+
# encrypt again
|
62
|
+
plaintext_attributes.each do |attr, value|
|
63
|
+
send("#{attr}=", value)
|
64
|
+
end
|
65
|
+
|
66
|
+
# update atomically
|
67
|
+
save!
|
48
68
|
end
|
49
69
|
end
|
50
70
|
end
|