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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: dcfe0e3ccdefdbde5b05b879a8f2badf3337fd0e
4
- data.tar.gz: 8866251c5210f0c67845f99a221931db0a77446f
3
+ metadata.gz: 844c888ff1036e25341d665534193770b363f2d1
4
+ data.tar.gz: 042e30c929e4db3e7d7c18d692cc78ce220932c7
5
5
  SHA512:
6
- metadata.gz: 664d6f021217bbd3e3740943bc91b60d1ad7af84c6f2081512f03956ba48488a98666a94417a61a1e68a15ccd36a8a4b31c541a6c40422a7cb8f21f079761d0f
7
- data.tar.gz: dbd76c23ee501e1e7c8cb28d4cd1e45430e6ea3904ef4afa8053973b034f8637627c01646e9b5782079f24c151702aa2e3006940df99f572b73be9a8adad7a56
6
+ metadata.gz: 14e1be577cf7639c7f4eea48f1b03a56315adcd82d6efc4d38ecf6662793947202c4ded1f0d674eb860b60da4e8081a65515949a3810384c6c7b91d666d8bc9a
7
+ data.tar.gz: 359927d6b27361c62434e49be3b1a90d9498119d73a5f098f903eadbad4afef3a879bb61ee19d8f8d41753a91e11cbe461a4b79a307d6a4b06238a5908a9cbea
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.1.1
2
+
3
+ - Added key rotation
4
+ - Added support for multiple keys per record
5
+
1
6
  ## 0.1.0
2
7
 
3
8
  - First release
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 of the issues and it’s easy to use them together.
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
- 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.
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
- Read more about [encryption context here](http://docs.aws.amazon.com/kms/latest/developerguide/encryption-context.html).
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
- ## How It Works
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
- KMS allows you to generate data keys from your master key. Data keys are encrypted and stored with each record. Whenever you want to decrypt data, you need to first decrypt the data key with KMS. Once you have the decrypted key, you can then use it to decrypt the data with attr_encrypted.
185
+ attr_encrypted :email, key: :kms_key
186
+ attr_encrypted :phone, key: :kms_key_phone
187
+ end
188
+ ```
68
189
 
69
- ## TODO
190
+ For context, use:
70
191
 
71
- - add support for multiple data keys per record
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
 
@@ -1,3 +1,3 @@
1
1
  module KmsEncrypted
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
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
- class << self
16
- attr_accessor :kms_key_id
17
- end
18
- self.kms_key_id = key_id
17
+ define_method(key_method) do
18
+ instance_var = "@#{key_method}"
19
19
 
20
- def kms_key
21
- unless @kms_key
22
- key_id = self.class.kms_key_id
23
- context = respond_to?(:kms_encryption_context) ? kms_encryption_context : {}
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 encrypted_kms_key
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
- self.encrypted_kms_key = [resp.ciphertext_blob].pack(default_encoding)
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 @kms_key
38
- ciphertext = encrypted_kms_key.unpack(default_encoding).first
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
- @kms_key = resp.plaintext
43
+ instance_variable_set(instance_var, resp.plaintext)
44
44
  end
45
45
  end
46
46
 
47
- @kms_key
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kms_encrypted
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane