active_kms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4ef69ec3793d206e0f6dc11c39de7d61af017d4c743441c20592411fab4ed54c
4
+ data.tar.gz: 7ddc9779bf39ce4dbbcbb52bb4f58a33407916cb58865de66aa761b4dd0da12a
5
+ SHA512:
6
+ metadata.gz: 8285f7d2a15d25507104264c66c29dfbc6fe745d0161a860d8cd9115f75b1748d574cdcf3e5a07557397f95fd51188bb7795eb2742c442cf37f6837502ef872e
7
+ data.tar.gz: 4240335bc026f1f36bdacdeb781b205cb4f809f81694bb83a3d69e10ee07e360ca50be999da68aa3236427b579f87c40998d7d8395d0d2438664fc15e300db1a
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 (2021-12-14)
2
+
3
+ - First release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Andrew Kane
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # Active KMS
2
+
3
+ Simple, secure key management for [Active Record encryption](https://edgeguides.rubyonrails.org/active_record_encryption.html)
4
+
5
+ **Note:** This project is experimental until Rails 7 is released. At the moment, encryption requires three encryption requests and one decryption request. See [this Rails issue](https://github.com/rails/rails/issues/42388) for more info. As a result, there’s no way to grant encryption and decryption permission separately.
6
+
7
+ [![Build Status](https://github.com/ankane/active_kms/workflows/build/badge.svg?branch=master)](https://github.com/ankane/active_kms/actions)
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application’s Gemfile:
12
+
13
+ ```ruby
14
+ gem "active_kms"
15
+ ```
16
+
17
+ And follow the instructions for your key management service:
18
+
19
+ - [AWS KMS](#aws-kms)
20
+ - [Google Cloud KMS](#google-cloud-kms)
21
+ - [Vault](#vault)
22
+
23
+ ### AWS KMS
24
+
25
+ Add this line to your application’s Gemfile:
26
+
27
+ ```ruby
28
+ gem "aws-sdk-kms"
29
+ ```
30
+
31
+ 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.
32
+
33
+ Create a [KMS master key](https://console.aws.amazon.com/kms/home#/kms/keys) and set it in your environment along with your AWS credentials ([dotenv](https://github.com/bkeepers/dotenv) is great for this)
34
+
35
+ ```sh
36
+ KMS_KEY_ID=alias/my-key
37
+ AWS_ACCESS_KEY_ID=...
38
+ AWS_SECRET_ACCESS_KEY=...
39
+ ```
40
+
41
+ And add to `config/application.rb`:
42
+
43
+ ```ruby
44
+ config.active_record.encryption.key_provider = ActiveKms::AwsKeyProvider.new(key_id: ENV["KMS_KEY_ID"])
45
+ ```
46
+
47
+ ### Google Cloud KMS
48
+
49
+ Add this line to your application’s Gemfile:
50
+
51
+ ```ruby
52
+ gem "google-cloud-kms"
53
+ ```
54
+
55
+ 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.
56
+
57
+ 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)
58
+
59
+ ```sh
60
+ KMS_KEY_ID=projects/my-project/locations/global/keyRings/my-key-ring/cryptoKeys/my-key
61
+ ```
62
+
63
+ And add to `config/application.rb`:
64
+
65
+ ```ruby
66
+ config.active_record.encryption.key_provider = ActiveKms::GoogleCloudKeyProvider.new(key_id: ENV["KMS_KEY_ID"])
67
+ ```
68
+
69
+ ### Vault
70
+
71
+ Add this line to your application’s Gemfile:
72
+
73
+ ```ruby
74
+ gem "vault"
75
+ ```
76
+
77
+ Enable the [transit](https://www.vaultproject.io/docs/secrets/transit/index.html) secrets engine
78
+
79
+ ```sh
80
+ vault secrets enable transit
81
+ ```
82
+
83
+ And create a key
84
+
85
+ ```sh
86
+ vault write -f transit/keys/my-key
87
+ ```
88
+
89
+ Set it in your environment along with your Vault credentials ([dotenv](https://github.com/bkeepers/dotenv) is great for this)
90
+
91
+ ```sh
92
+ KMS_KEY_ID=my-key
93
+ VAULT_ADDR=http://127.0.0.1:8200
94
+ VAULT_TOKEN=secret
95
+ ```
96
+
97
+ And add to `config/application.rb`:
98
+
99
+ ```ruby
100
+ config.active_record.encryption.key_provider = ActiveKms::VaultKeyProvider.new(key_id: ENV["KMS_KEY_ID"])
101
+ ```
102
+
103
+ ## Per-Attribute Keys
104
+
105
+ Specify per-attribute keys
106
+
107
+ ```ruby
108
+ class User < ApplicationRecord
109
+ encrypts :email, key_provider: ActiveKms::AwsKeyProvider.new(key_id: "...")
110
+ end
111
+ ```
112
+
113
+ ## Testing
114
+
115
+ For testing, you can prevent network calls to KMS by adding to `config/environments/test.rb`:
116
+
117
+ ```ruby
118
+ config.active_record.encryption.key_provider = ActiveKms::TestKeyProvider.new
119
+ ```
120
+
121
+ ## Key Rotation
122
+
123
+ Key management services allow you to rotate the master key without any code changes.
124
+
125
+ - For AWS KMS, you can use [automatic key rotation](https://docs.aws.amazon.com/kms/latest/developerguide/rotate-keys.html)
126
+ - For Google Cloud, use the Google Cloud Console or API
127
+ - For Vault, use:
128
+
129
+ ```sh
130
+ vault write -f transit/keys/my-key/rotate
131
+ ```
132
+
133
+ New data will be encrypted with the new master key version.
134
+
135
+ ### Switching Keys
136
+
137
+ You can change keys within your current KMS or move to a different KMS without downtime.
138
+
139
+ Set globally in `config/application.rb`:
140
+
141
+ ```ruby
142
+ config.active_record.encryption.previous = [{key_provider: ActiveKms::AwsKeyProvider.new(key_id: "...")}]
143
+ ```
144
+
145
+ Or per-attribute:
146
+
147
+ ```ruby
148
+ class User < ApplicationRecord
149
+ encrypts :email, previous: [{key_provider: ActiveKms::AwsKeyProvider.new(key_id: "...")}]
150
+ end
151
+ ```
152
+
153
+ ## Reference
154
+
155
+ Specify a client
156
+
157
+ ```ruby
158
+ ActiveKms::AwsKeyProvider.new(client: Aws::KMS::Client.new, ...)
159
+ # or
160
+ ActiveKms::GoogleCloudKeyProvider.new(client: Google::Cloud::Kms.key_management_service, ...)
161
+ # or
162
+ ActiveKms::VaultKeyProvider.new(client: Vault::Client.new, ...)
163
+ ```
164
+
165
+ ## History
166
+
167
+ View the [changelog](https://github.com/ankane/active_kms/blob/master/CHANGELOG.md)
168
+
169
+ ## Contributing
170
+
171
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
172
+
173
+ - [Report bugs](https://github.com/ankane/active_kms/issues)
174
+ - Fix bugs and [submit pull requests](https://github.com/ankane/active_kms/pulls)
175
+ - Write, clarify, or fix documentation
176
+ - Suggest or add new features
177
+
178
+ To get started with development:
179
+
180
+ ```sh
181
+ git clone https://github.com/ankane/active_kms.git
182
+ cd active_kms
183
+ bundle install
184
+ bundle exec rake test
185
+ ```
@@ -0,0 +1,28 @@
1
+ module ActiveKms
2
+ class AwsKeyProvider < BaseKeyProvider
3
+ private
4
+
5
+ def default_client
6
+ Aws::KMS::Client.new(
7
+ retry_limit: 1,
8
+ http_open_timeout: 2,
9
+ http_read_timeout: 2
10
+ )
11
+ end
12
+
13
+ def encrypt(key_id, data_key)
14
+ client.encrypt(key_id: key_id, plaintext: data_key).ciphertext_blob
15
+ end
16
+
17
+ def decrypt(_, encrypted_data_key)
18
+ client.decrypt(ciphertext_blob: encrypted_data_key).plaintext
19
+ end
20
+
21
+ # key is stored in ciphertext so don't need to store reference
22
+ # reference could be useful for multiple AWS clients
23
+ # so consider an option in the future
24
+ def key_id_header
25
+ "aws"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,47 @@
1
+ module ActiveKms
2
+ class BaseKeyProvider
3
+ attr_reader :key_id, :client
4
+
5
+ def initialize(key_id:, client: nil)
6
+ @key_id = key_id
7
+ @client = client || default_client
8
+ end
9
+
10
+ def encryption_key
11
+ data_key = ActiveRecord::Encryption.key_generator.generate_random_key
12
+ encrypted_data_key =
13
+ ActiveSupport::Notifications.instrument("encrypt.active_kms") do
14
+ encrypt(key_id, data_key)
15
+ end
16
+
17
+ key = ActiveRecord::Encryption::Key.new(data_key)
18
+ key.public_tags.encrypted_data_key = encrypted_data_key
19
+ key.public_tags.encrypted_data_key_id = key_id_header
20
+ key
21
+ end
22
+
23
+ def decryption_keys(encrypted_message)
24
+ return [] if encrypted_message.headers.encrypted_data_key_id != key_id_header
25
+
26
+ encrypted_data_key = encrypted_message.headers.encrypted_data_key
27
+ # rescue errors to try previous keys
28
+ # rescue outside Active Support notification for more intuitive output
29
+ begin
30
+ data_key =
31
+ ActiveSupport::Notifications.instrument("decrypt.active_kms") do
32
+ decrypt(key_id, encrypted_data_key)
33
+ end
34
+ [ActiveRecord::Encryption::Key.new(data_key)]
35
+ rescue => e
36
+ warn "[active_kms] #{e.class.name}: #{e.message}"
37
+ []
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def key_id_header
44
+ @key_id_header ||= "#{prefix}/#{Digest::SHA1.hexdigest(key_id).first(4)}"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ module ActiveKms
2
+ class GoogleCloudKeyProvider < BaseKeyProvider
3
+ private
4
+
5
+ def default_client
6
+ require "google/cloud/kms"
7
+
8
+ Google::Cloud::Kms.key_management_service do |config|
9
+ config.timeout = 2
10
+ end
11
+ end
12
+
13
+ def encrypt(key_id, data_key)
14
+ client.encrypt(name: key_id, plaintext: data_key).ciphertext
15
+ end
16
+
17
+ def decrypt(key_id, encrypted_data_key)
18
+ client.decrypt(name: key_id, ciphertext: encrypted_data_key).plaintext
19
+ end
20
+
21
+ def prefix
22
+ "gc"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ module ActiveKms
2
+ class LogSubscriber < ActiveSupport::LogSubscriber
3
+ def decrypt(event)
4
+ return unless logger.debug?
5
+
6
+ name = "Decrypt Data Key (#{event.duration.round(1)}ms)"
7
+ debug " #{color(name, YELLOW, true)}"
8
+ end
9
+
10
+ def encrypt(event)
11
+ return unless logger.debug?
12
+
13
+ name = "Encrypt Data Key (#{event.duration.round(1)}ms)"
14
+ debug " #{color(name, YELLOW, true)}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module ActiveKms
2
+ class TestKeyProvider < BaseKeyProvider
3
+ def initialize
4
+ end
5
+
6
+ private
7
+
8
+ def encrypt(_, data_key)
9
+ data_key
10
+ end
11
+
12
+ def decrypt(_, encrypted_data_key)
13
+ encrypted_data_key
14
+ end
15
+
16
+ def key_id_header
17
+ "test"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveKms
2
+ class VaultKeyProvider < BaseKeyProvider
3
+ private
4
+
5
+ def default_client
6
+ Vault::Client.new
7
+ end
8
+
9
+ def encrypt(key_id, data_key)
10
+ client.logical.write("transit/encrypt/#{key_id}", plaintext: Base64.encode64(data_key)).data[:ciphertext]
11
+ end
12
+
13
+ def decrypt(key_id, encrypted_data_key)
14
+ Base64.decode64(client.logical.write("transit/decrypt/#{key_id}", ciphertext: encrypted_data_key).data[:plaintext])
15
+ end
16
+
17
+ # could store entire key_id in key_id_header but prefer reference
18
+ def prefix
19
+ "vt"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveKms
2
+ VERSION = "0.1.0"
3
+ end
data/lib/active_kms.rb ADDED
@@ -0,0 +1,19 @@
1
+ # dependencies
2
+ require "active_support"
3
+
4
+ # modules
5
+ require "active_kms/base_key_provider"
6
+ require "active_kms/log_subscriber"
7
+ require "active_kms/version"
8
+
9
+ # providers
10
+ require "active_kms/aws_key_provider"
11
+ require "active_kms/google_cloud_key_provider"
12
+ require "active_kms/test_key_provider"
13
+ require "active_kms/vault_key_provider"
14
+
15
+ module ActiveKms
16
+ class Error < StandardError; end
17
+ end
18
+
19
+ ActiveKms::LogSubscriber.attach_to :active_kms
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_kms
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Kane
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-12-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.0.rc3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.0.rc3
27
+ description:
28
+ email: andrew@ankane.org
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - CHANGELOG.md
34
+ - LICENSE.txt
35
+ - README.md
36
+ - lib/active_kms.rb
37
+ - lib/active_kms/aws_key_provider.rb
38
+ - lib/active_kms/base_key_provider.rb
39
+ - lib/active_kms/google_cloud_key_provider.rb
40
+ - lib/active_kms/log_subscriber.rb
41
+ - lib/active_kms/test_key_provider.rb
42
+ - lib/active_kms/vault_key_provider.rb
43
+ - lib/active_kms/version.rb
44
+ homepage: https://github.com/ankane/active_kms
45
+ licenses:
46
+ - MIT
47
+ metadata: {}
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '2.6'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.2.32
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: Simple, secure key management for Active Record encryption
67
+ test_files: []