attr_keyring 0.5.4 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/README.md +117 -45
- data/lib/attr_keyring.rb +3 -3
- data/lib/attr_keyring/version.rb +1 -1
- data/lib/keyring.rb +24 -6
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ee86bffa6e27d8523d423c8e8bfefa90a15733fdda28a60155a71fd67a794917
|
4
|
+
data.tar.gz: 340174053331dc2447f2d980ec97d3a4b429f09ed8052ef991908a2de90ad872
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f4d6593813ab7f4e2e8672108d40b44f068c70fc8ccc59c83eae84bba0851ba3e6996633ef66b87b83f2f5c93038b573062b446e93045902623aba5dbcd653c
|
7
|
+
data.tar.gz: e2379acd02af797c2dcf900957404eb6f8552fe1b11f472c4ea8b5a140b06fbda868eec13e65fa998cc27d11274435e329aec3a12b59f516ecc1dddf779b4f4e
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -10,7 +10,10 @@
|
|
10
10
|
|
11
11
|
N.B.: attr_keyring is *not* for encrypting passwords--for that, you should use something like [bcrypt](https://github.com/codahale/bcrypt-ruby). It's meant for encrypting sensitive data you will need to access in plain text (e.g. storing OAuth token from users). Passwords do not fall in that category.
|
12
12
|
|
13
|
-
This library is heavily inspired by
|
13
|
+
This library is heavily inspired by
|
14
|
+
[attr_vault](https://github.com/uhoh-itsmaciek/attr_vault), and can read
|
15
|
+
encrypted messages if you encode them in base64
|
16
|
+
(e.g. `Base64.strict_encode64(encrypted_by_attr_vault)`).
|
14
17
|
|
15
18
|
## Installation
|
16
19
|
|
@@ -36,7 +39,10 @@ Or install it yourself as:
|
|
36
39
|
gem "attr_keyring"
|
37
40
|
require "keyring"
|
38
41
|
|
39
|
-
keyring = Keyring.new(
|
42
|
+
keyring = Keyring.new(
|
43
|
+
{"1" => "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M="},
|
44
|
+
digest_salt: "<custom salt>"
|
45
|
+
)
|
40
46
|
|
41
47
|
# STEP 1: Encrypt message using latest encryption key.
|
42
48
|
encrypted, keyring_id, digest = keyring.encrypt("super secret")
|
@@ -52,30 +58,37 @@ puts "✉️ #{decrypted}"
|
|
52
58
|
|
53
59
|
#### Change encryption algorithm
|
54
60
|
|
55
|
-
You can choose between `AES-128-CBC`, `AES-192-CBC` and `AES-256-CBC`. By
|
56
|
-
|
57
|
-
To specify the encryption algorithm, set the `encryption` option. The following example uses `AES-256-CBC`.
|
61
|
+
You can choose between `AES-128-CBC`, `AES-192-CBC` and `AES-256-CBC`. By
|
62
|
+
default, `AES-128-CBC` will be used.
|
58
63
|
|
59
|
-
|
60
|
-
|
64
|
+
To specify the encryption algorithm, set the `encryption` option. The following
|
65
|
+
example uses `AES-256-CBC`.
|
61
66
|
|
62
|
-
|
63
|
-
|
67
|
+
```ruby
|
68
|
+
keyring = Keyring.new(
|
69
|
+
"1" => "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M=",
|
70
|
+
encryptor: Keyring::Encryptor::AES256CBC,
|
71
|
+
digest_salt: "<custom salt>"
|
72
|
+
)
|
64
73
|
```
|
65
74
|
|
66
75
|
### Configuration
|
67
76
|
|
68
77
|
As far as database schema goes:
|
69
78
|
|
70
|
-
1. You'll need a column to track the key that was used for encryption; by
|
71
|
-
|
72
|
-
|
79
|
+
1. You'll need a column to track the key that was used for encryption; by
|
80
|
+
default it's called `keyring_id`.
|
81
|
+
2. Every encrypted column must follow the name `encrypted_<column name>`.
|
82
|
+
3. Optionally, you can also have a `<column name>_digest` to help with searching
|
83
|
+
(see Lookup section below).
|
73
84
|
|
74
|
-
As far as model configuration goes, they're pretty similar, as you can see
|
85
|
+
As far as model configuration goes, they're pretty similar, as you can see
|
86
|
+
below:
|
75
87
|
|
76
88
|
#### ActiveRecord
|
77
89
|
|
78
|
-
From Rails 5+, ActiveRecord models now inherit from `ApplicationRecord` instead.
|
90
|
+
From Rails 5+, ActiveRecord models now inherit from `ApplicationRecord` instead.
|
91
|
+
This is how you set it up:
|
79
92
|
|
80
93
|
```ruby
|
81
94
|
class ApplicationRecord < ActiveRecord::Base
|
@@ -86,7 +99,8 @@ end
|
|
86
99
|
|
87
100
|
#### Sequel
|
88
101
|
|
89
|
-
Sequel doesn't have an abstract model class (but it could), so you can set up
|
102
|
+
Sequel doesn't have an abstract model class (but it could), so you can set up
|
103
|
+
the model class directly like the following:
|
90
104
|
|
91
105
|
```ruby
|
92
106
|
class User < Sequel::Model
|
@@ -96,16 +110,20 @@ end
|
|
96
110
|
|
97
111
|
### Defining encrypted attributes
|
98
112
|
|
99
|
-
To set up your model, you have to define the keyring (set of encryption keys)
|
113
|
+
To set up your model, you have to define the keyring (set of encryption keys)
|
114
|
+
and the attributes that will be encrypted. Both ActiveRecord and Sequel have the
|
115
|
+
same API, so the examples below work for both ORMs.
|
100
116
|
|
101
117
|
```ruby
|
102
118
|
class User < ApplicationRecord
|
103
|
-
attr_keyring ENV["USER_KEYRING"]
|
119
|
+
attr_keyring ENV["USER_KEYRING"],
|
120
|
+
digest_salt: "<custom salt>"
|
104
121
|
attr_encrypt :twitter_oauth_token, :social_security_number
|
105
122
|
end
|
106
123
|
```
|
107
124
|
|
108
|
-
The code above will encrypt your columns with the current key. If you're
|
125
|
+
The code above will encrypt your columns with the current key. If you're
|
126
|
+
updating a record, then the column will be migrated to the latest key available.
|
109
127
|
|
110
128
|
You can use the model as you would normally do.
|
111
129
|
|
@@ -126,20 +144,27 @@ user.encrypted_email
|
|
126
144
|
|
127
145
|
### Encryption
|
128
146
|
|
129
|
-
By default, AES-128-CBC is the algorithm used for encryption. This algorithm
|
147
|
+
By default, AES-128-CBC is the algorithm used for encryption. This algorithm
|
148
|
+
uses 16 bytes keys, but you're required to use a key that's double the size
|
149
|
+
because half of that keys will be used to generate the HMAC. The first 16 bytes
|
150
|
+
will be used as the encryption key, and the last 16 bytes will be used to
|
151
|
+
generate the HMAC.
|
130
152
|
|
131
|
-
Using random data base64-encoded is the recommended way. You can easily generate
|
153
|
+
Using random data base64-encoded is the recommended way. You can easily generate
|
154
|
+
keys by using the following command:
|
132
155
|
|
133
156
|
```console
|
134
157
|
$ dd if=/dev/urandom bs=32 count=1 2>/dev/null | openssl base64 -A
|
135
158
|
qUjOJFgZsZbTICsN0TMkKqUvSgObYxnkHDsazTqE5tM=
|
136
159
|
```
|
137
160
|
|
138
|
-
Include the result of this command in the `value` section of the key description
|
161
|
+
Include the result of this command in the `value` section of the key description
|
162
|
+
in the keyring. Half this key is used for encryption, and half for the HMAC.
|
139
163
|
|
140
164
|
#### Key size
|
141
165
|
|
142
|
-
The key size depends on the algorithm being used. The key size should be double
|
166
|
+
The key size depends on the algorithm being used. The key size should be double
|
167
|
+
the size as half of it is used for HMAC computation.
|
143
168
|
|
144
169
|
- `aes-128-cbc`: 16 bytes (encryption) + 16 bytes (HMAC).
|
145
170
|
- `aes-192-cbc`: 24 bytes (encryption) + 24 bytes (HMAC).
|
@@ -147,13 +172,24 @@ The key size depends on the algorithm being used. The key size should be double
|
|
147
172
|
|
148
173
|
#### About the encrypted message
|
149
174
|
|
150
|
-
Initialization vectors (IV) should be unpredictable and unique; ideally, they
|
175
|
+
Initialization vectors (IV) should be unpredictable and unique; ideally, they
|
176
|
+
will be cryptographically random. They do not have to be secret: IVs are
|
177
|
+
typically just added to ciphertext messages unencrypted. It may sound
|
178
|
+
contradictory that something has to be unpredictable and unique, but does not
|
179
|
+
have to be secret; it is important to remember that an attacker must not be able
|
180
|
+
to predict ahead of time what a given IV will be.
|
151
181
|
|
152
|
-
With that in mind, _attr_keyring_ uses `base64(hmac(unencrypted iv + encrypted
|
182
|
+
With that in mind, _attr_keyring_ uses `base64(hmac(unencrypted iv + encrypted
|
183
|
+
message) + unencrypted iv + encrypted message)` as the final message. If you're
|
184
|
+
planning to migrate from other encryption mechanisms or read encrypted values
|
185
|
+
from the database without using _attr_keyring_, make sure you account for this.
|
186
|
+
The HMAC is 32-bytes long and the IV is 16-bytes long.
|
153
187
|
|
154
188
|
### Keyring
|
155
189
|
|
156
|
-
Keys are managed through a keyring--a short JSON document describing your
|
190
|
+
Keys are managed through a keyring--a short JSON document describing your
|
191
|
+
encryption keys. The keyring must be a JSON object mapping numeric ids of the
|
192
|
+
keys to the key values. A keyring must have at least one key. For example:
|
157
193
|
|
158
194
|
```json
|
159
195
|
{
|
@@ -162,11 +198,14 @@ Keys are managed through a keyring--a short JSON document describing your encryp
|
|
162
198
|
}
|
163
199
|
```
|
164
200
|
|
165
|
-
The `id` is used to track which key encrypted which piece of data; a key with a
|
201
|
+
The `id` is used to track which key encrypted which piece of data; a key with a
|
202
|
+
larger id is assumed to be newer. The value is the actual bytes of the
|
203
|
+
encryption key.
|
166
204
|
|
167
205
|
#### Dynamically loading keyring
|
168
206
|
|
169
|
-
If you're using Rails 5.2+, you can use credentials to define your keyring.
|
207
|
+
If you're using Rails 5.2+, you can use credentials to define your keyring.
|
208
|
+
Your `credentials.yml` must be define like the following:
|
170
209
|
|
171
210
|
```yaml
|
172
211
|
user_keyring:
|
@@ -174,12 +213,14 @@ user_keyring:
|
|
174
213
|
2: "r6AfOeilPDJomFsiOXLdfQ=="
|
175
214
|
```
|
176
215
|
|
177
|
-
Then you can setup your model by using
|
216
|
+
Then you can setup your model by using
|
217
|
+
`attr_keyring Rails.application.credentials.user_keyring`.
|
178
218
|
|
179
|
-
Other possibilities (e.g. the keyring file is provided by configuration
|
219
|
+
Other possibilities (e.g. the keyring file is provided by configuration
|
220
|
+
management):
|
180
221
|
|
181
|
-
- `attr_keyring YAML.load_file(keyring_file)`
|
182
|
-
- `attr_keyring JSON.parse(File.read(keyring_file))`.
|
222
|
+
- `attr_keyring YAML.load_file(keyring_file), digest_salt: "<custom salt>"`
|
223
|
+
- `attr_keyring JSON.parse(File.read(keyring_file)), digest_salt: "<custom salt>"`.
|
183
224
|
|
184
225
|
### Lookup
|
185
226
|
|
@@ -189,17 +230,25 @@ One tricky aspect of encryption is looking up records by known secret. E.g.,
|
|
189
230
|
User.where(email: "john@example.com")
|
190
231
|
```
|
191
232
|
|
192
|
-
is trivial with plain text fields, but impossible with the model defined as
|
233
|
+
is trivial with plain text fields, but impossible with the model defined as
|
234
|
+
above.
|
193
235
|
|
194
|
-
If a column `<attribute>_digest` exists, then a SHA1 digest from the value will
|
236
|
+
If a column `<attribute>_digest` exists, then a SHA1 digest from the value will
|
237
|
+
be saved. This will allow you to lookup by that value instead and add unique
|
238
|
+
indexes. You don't have to use a hashing salt, but it's highly recommended; this
|
239
|
+
way you can avoid leaking your users' info via rainbow tables.
|
195
240
|
|
196
241
|
```ruby
|
197
|
-
User.where(email:
|
242
|
+
User.where(email: User.keyring.digest("john@example.com")).first
|
198
243
|
```
|
199
244
|
|
200
245
|
### Key Rotation
|
201
246
|
|
202
|
-
Because attr_keyring uses a keyring, with access to multiple keys at once, key
|
247
|
+
Because attr_keyring uses a keyring, with access to multiple keys at once, key
|
248
|
+
rotation is fairly straightforward: if you add a key to the keyring with a
|
249
|
+
higher id than any other key, that key will automatically be used for encryption
|
250
|
+
when records are either created or updated. Any keys that are no longer in use
|
251
|
+
can be safely removed from the keyring.
|
203
252
|
|
204
253
|
To check if an existing key with id `123` is still in use, run:
|
205
254
|
|
@@ -208,7 +257,8 @@ To check if an existing key with id `123` is still in use, run:
|
|
208
257
|
User.where(keyring_id: 123).empty?
|
209
258
|
```
|
210
259
|
|
211
|
-
You may not want to wait for records to be updated (e.g. key leaking). In that
|
260
|
+
You may not want to wait for records to be updated (e.g. key leaking). In that
|
261
|
+
case, you can rollout a key rotation:
|
212
262
|
|
213
263
|
```ruby
|
214
264
|
User.where(keyring_id: 1234).find_each do |user|
|
@@ -218,12 +268,18 @@ end
|
|
218
268
|
|
219
269
|
### What if I don't use ActiveRecord/Sequel?
|
220
270
|
|
221
|
-
You can also leverage the encryption mechanism of `attr_keyring` totally
|
271
|
+
You can also leverage the encryption mechanism of `attr_keyring` totally
|
272
|
+
decoupled from ActiveRecord/Sequel. First, make sure you load `keyring` instead.
|
273
|
+
Then you can create a keyring to encrypt/decrypt strings, without even touching
|
274
|
+
the database.
|
222
275
|
|
223
276
|
```ruby
|
224
277
|
require "keyring"
|
225
278
|
|
226
|
-
keyring = Keyring.new(
|
279
|
+
keyring = Keyring.new(
|
280
|
+
{"1" => "QSXyoiRDPoJmfkJUZ4hJeQ=="},
|
281
|
+
digest_salt: "<custom salt>"
|
282
|
+
)
|
227
283
|
|
228
284
|
encrypted, keyring_id, digest = keyring.encrypt("super secret")
|
229
285
|
|
@@ -244,26 +300,42 @@ puts decrypted
|
|
244
300
|
|
245
301
|
### Exchange data with Node.js
|
246
302
|
|
247
|
-
If you use Node.js, you may be interested in
|
303
|
+
If you use Node.js, you may be interested in
|
304
|
+
<https://github.com/fnando/keyring-node>, which is able to read and write
|
305
|
+
messages using the same format.
|
248
306
|
|
249
307
|
## Development
|
250
308
|
|
251
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
309
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
310
|
+
`rake test` to run the tests. You can also run `bin/console` for an interactive
|
311
|
+
prompt that will allow you to experiment.
|
252
312
|
|
253
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To
|
313
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
314
|
+
release a new version, update the version number in `version.rb`, and then run
|
315
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
316
|
+
git commits and tags, and push the `.gem` file to
|
317
|
+
[rubygems.org](https://rubygems.org).
|
254
318
|
|
255
319
|
## Contributing
|
256
320
|
|
257
|
-
Bug reports and pull requests are welcome on GitHub at
|
321
|
+
Bug reports and pull requests are welcome on GitHub at
|
322
|
+
https://github.com/fnando/attr_keyring. This project is intended to be a safe,
|
323
|
+
welcoming space for collaboration, and contributors are expected to adhere to
|
324
|
+
the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
258
325
|
|
259
326
|
## License
|
260
327
|
|
261
|
-
The gem is available as open source under the terms of the
|
328
|
+
The gem is available as open source under the terms of the
|
329
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
262
330
|
|
263
331
|
## Icon
|
264
332
|
|
265
|
-
Icon made by [Icongeek26](https://www.flaticon.com/authors/icongeek26)
|
333
|
+
Icon made by [Icongeek26](https://www.flaticon.com/authors/icongeek26)
|
334
|
+
from [Flaticon](https://www.flaticon.com/) is licensed by Creative Commons BY
|
335
|
+
3.0.
|
266
336
|
|
267
337
|
## Code of Conduct
|
268
338
|
|
269
|
-
Everyone interacting in the attr_keyring project’s codebases, issue trackers,
|
339
|
+
Everyone interacting in the attr_keyring project’s codebases, issue trackers,
|
340
|
+
chat rooms and mailing lists is expected to follow the
|
341
|
+
[code of conduct](https://github.com/fnando/attr_keyring/blob/master/CODE_OF_CONDUCT.md).
|
data/lib/attr_keyring.rb
CHANGED
@@ -26,7 +26,7 @@ module AttrKeyring
|
|
26
26
|
end
|
27
27
|
|
28
28
|
self.encrypted_attributes = []
|
29
|
-
self.keyring = Keyring.new({})
|
29
|
+
self.keyring = Keyring.new({}, digest_salt: "")
|
30
30
|
self.keyring_column_name = :keyring_id
|
31
31
|
end
|
32
32
|
end
|
@@ -40,8 +40,8 @@ module AttrKeyring
|
|
40
40
|
subclass.keyring_column_name = keyring_column_name
|
41
41
|
end
|
42
42
|
|
43
|
-
def attr_keyring(keyring,
|
44
|
-
self.keyring = Keyring.new(keyring,
|
43
|
+
def attr_keyring(keyring, options = {})
|
44
|
+
self.keyring = Keyring.new(keyring, options)
|
45
45
|
end
|
46
46
|
|
47
47
|
def attr_encrypt(*attributes)
|
data/lib/attr_keyring/version.rb
CHANGED
data/lib/keyring.rb
CHANGED
@@ -12,10 +12,19 @@ module Keyring
|
|
12
12
|
InvalidSecret = Class.new(StandardError)
|
13
13
|
EmptyKeyring = Class.new(StandardError)
|
14
14
|
InvalidAuthentication = Class.new(StandardError)
|
15
|
+
MissingDigestSalt = Class.new(StandardError) do
|
16
|
+
def message
|
17
|
+
%w[
|
18
|
+
Please provide :digest_salt;
|
19
|
+
you can disable this error by explicitly passing an empty string.
|
20
|
+
].join(" ")
|
21
|
+
end
|
22
|
+
end
|
15
23
|
|
16
24
|
class Base
|
17
|
-
def initialize(keyring,
|
18
|
-
@encryptor = encryptor
|
25
|
+
def initialize(keyring, options)
|
26
|
+
@encryptor = options[:encryptor]
|
27
|
+
@digest_salt = options[:digest_salt]
|
19
28
|
@keyring = keyring.map do |id, value|
|
20
29
|
Key.new(id, value, @encryptor.key_size)
|
21
30
|
end
|
@@ -44,13 +53,12 @@ module Keyring
|
|
44
53
|
|
45
54
|
def encrypt(message, keyring_id = nil)
|
46
55
|
keyring_id ||= current_key&.id
|
47
|
-
digest = Digest::SHA1.hexdigest(message)
|
48
56
|
key = self[keyring_id]
|
49
57
|
|
50
58
|
[
|
51
59
|
@encryptor.encrypt(key, message),
|
52
60
|
keyring_id,
|
53
|
-
digest
|
61
|
+
digest(message)
|
54
62
|
]
|
55
63
|
end
|
56
64
|
|
@@ -58,9 +66,19 @@ module Keyring
|
|
58
66
|
key = self[keyring_id]
|
59
67
|
@encryptor.decrypt(key, message)
|
60
68
|
end
|
69
|
+
|
70
|
+
def digest(message)
|
71
|
+
Digest::SHA1.hexdigest("#{message}#{@digest_salt}")
|
72
|
+
end
|
61
73
|
end
|
62
74
|
|
63
|
-
def self.new(keyring,
|
64
|
-
|
75
|
+
def self.new(keyring, options = {})
|
76
|
+
options = {
|
77
|
+
encryptor: Encryptor::AES::AES128CBC
|
78
|
+
}.merge(options)
|
79
|
+
|
80
|
+
raise MissingDigestSalt if options[:digest_salt].nil?
|
81
|
+
|
82
|
+
Base.new(keyring, options)
|
65
83
|
end
|
66
84
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attr_keyring
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nando Vieira
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-02-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -229,7 +229,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
229
229
|
- !ruby/object:Gem::Version
|
230
230
|
version: '0'
|
231
231
|
requirements: []
|
232
|
-
rubygems_version: 3.
|
232
|
+
rubygems_version: 3.1.2
|
233
233
|
signing_key:
|
234
234
|
specification_version: 4
|
235
235
|
summary: Simple encryption-at-rest plugin for ActiveRecord.
|