attr_keyring 0.5.4 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +3 -0
- data/.github/dependabot.yml +15 -0
- data/.github/workflows/tests.yml +68 -0
- data/.rubocop.yml +5 -2
- data/README.md +141 -51
- data/Rakefile +1 -1
- data/attr_keyring.gemspec +3 -4
- data/gemfiles/{5_2.gemfile → 6_1.gemfile} +1 -1
- data/gemfiles/7_0.gemfile +5 -0
- data/lib/attr_keyring/active_record.rb +1 -1
- data/lib/attr_keyring/encoders/json_encoder.rb +15 -0
- data/lib/attr_keyring/version.rb +1 -1
- data/lib/attr_keyring.rb +20 -11
- data/lib/keyring/encryptor/aes.rb +4 -1
- data/lib/keyring/key.rb +2 -2
- data/lib/keyring.rb +24 -6
- metadata +13 -9
- data/.travis.yml +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4a1335e867e0b79f0f8082b6cceaa30e40feeb7a9cb140dbb133989ad47af66f
|
4
|
+
data.tar.gz: fa5803034ce08ff55f515fc87e774eeb16b96597e229f55266bfdafd51225dd0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d2bc02c88a3871191cc41e32707452f1df9c50040991a8b09394d06a7a06d83a84b3574ac2e1e291e41cb8525d190213e31066eb9922440bbbe3426271829eb
|
7
|
+
data.tar.gz: '00086d53fd3bd74cc0ab4a1e5b511b02ddb081dc01531df75999461200c15b5e87069e0b38eb62f5c353ed121cb8f4657ec1e130aebfc0d254528a9365896389'
|
data/.github/FUNDING.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
# Documentation:
|
3
|
+
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
4
|
+
|
5
|
+
version: 2
|
6
|
+
updates:
|
7
|
+
- package-ecosystem: "github-actions"
|
8
|
+
directory: "/"
|
9
|
+
schedule:
|
10
|
+
interval: "daily"
|
11
|
+
|
12
|
+
- package-ecosystem: bundler
|
13
|
+
directory: "/"
|
14
|
+
schedule:
|
15
|
+
interval: "daily"
|
@@ -0,0 +1,68 @@
|
|
1
|
+
---
|
2
|
+
name: Tests
|
3
|
+
|
4
|
+
on:
|
5
|
+
pull_request:
|
6
|
+
push:
|
7
|
+
workflow_dispatch:
|
8
|
+
inputs: {}
|
9
|
+
|
10
|
+
jobs:
|
11
|
+
build:
|
12
|
+
name: Tests with Ruby ${{ matrix.ruby }} with ${{ matrix.gemfile }}
|
13
|
+
runs-on: "ubuntu-latest"
|
14
|
+
strategy:
|
15
|
+
fail-fast: false
|
16
|
+
matrix:
|
17
|
+
ruby: ["2.7", "3.0", "3.1"]
|
18
|
+
gemfile:
|
19
|
+
- gemfiles/7_0.gemfile
|
20
|
+
- gemfiles/6_1.gemfile
|
21
|
+
- gemfiles/6_0.gemfile
|
22
|
+
|
23
|
+
services:
|
24
|
+
postgres:
|
25
|
+
image: postgres:11.5
|
26
|
+
ports: ["5432:5432"]
|
27
|
+
options:
|
28
|
+
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
|
29
|
+
--health-retries 5
|
30
|
+
|
31
|
+
steps:
|
32
|
+
- uses: actions/checkout@v3.0.2
|
33
|
+
|
34
|
+
- uses: actions/cache@v3.0.1
|
35
|
+
with:
|
36
|
+
path: vendor/bundle
|
37
|
+
key: >
|
38
|
+
${{ runner.os }}-${{ matrix.ruby }}-gems-${{
|
39
|
+
hashFiles('**/attr_keyring.gemspec') }}
|
40
|
+
restore-keys: >
|
41
|
+
${{ runner.os }}-${{ matrix.ruby }}-gems-${{
|
42
|
+
hashFiles('**/attr_keyring.gemspec') }}
|
43
|
+
|
44
|
+
- name: Set up Ruby
|
45
|
+
uses: ruby/setup-ruby@v1
|
46
|
+
with:
|
47
|
+
ruby-version: ${{ matrix.ruby }}
|
48
|
+
|
49
|
+
- name: Install PostgreSQL 11 client
|
50
|
+
run: |
|
51
|
+
sudo apt-get -yqq install libpq-dev
|
52
|
+
|
53
|
+
- name: Install gem dependencies
|
54
|
+
env:
|
55
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
56
|
+
run: |
|
57
|
+
gem install bundler
|
58
|
+
bundle config path vendor/bundle
|
59
|
+
bundle update --jobs 4 --retry 3
|
60
|
+
|
61
|
+
- name: Run Tests
|
62
|
+
env:
|
63
|
+
PGHOST: localhost
|
64
|
+
PGUSER: postgres
|
65
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
66
|
+
run: |
|
67
|
+
psql -U postgres -c "create database test"
|
68
|
+
bundle exec rake
|
data/.rubocop.yml
CHANGED
@@ -3,12 +3,15 @@ inherit_gem:
|
|
3
3
|
rubocop-fnando: .rubocop.yml
|
4
4
|
|
5
5
|
AllCops:
|
6
|
-
TargetRubyVersion: 2.
|
6
|
+
TargetRubyVersion: 2.5
|
7
|
+
Exclude:
|
8
|
+
- vendor/**/*
|
9
|
+
- gemfiles/**/*
|
7
10
|
|
8
11
|
Metrics/AbcSize:
|
9
12
|
Enabled: false
|
10
13
|
|
11
|
-
|
14
|
+
Layout/LineLength:
|
12
15
|
Exclude:
|
13
16
|
- test/**/*
|
14
17
|
|
data/README.md
CHANGED
@@ -1,16 +1,21 @@
|
|
1
|
-
![attr_keyring: Simple encryption-at-rest with key rotation support for Ruby.](https://raw.githubusercontent.com/fnando/attr_keyring/
|
1
|
+
![attr_keyring: Simple encryption-at-rest with key rotation support for Ruby.](https://raw.githubusercontent.com/fnando/attr_keyring/main/attr_keyring.png)
|
2
2
|
|
3
3
|
<p align="center">
|
4
|
-
<a href="https://
|
4
|
+
<a href="https://github.com/fnando/attr_keyring/actions?query=workflow%3ATests"><img src="https://github.com/fnando/attr_keyring/workflows/Tests/badge.svg" alt="Tests"></a>
|
5
5
|
<a href="https://codeclimate.com/github/fnando/attr_keyring"><img src="https://codeclimate.com/github/fnando/attr_keyring/badges/gpa.svg" alt="Code Climate"></a>
|
6
|
-
<a href="https://codeclimate.com/github/fnando/attr_keyring/coverage"><img src="https://codeclimate.com/github/fnando/attr_keyring/badges/coverage.svg" alt="Test Coverage"></a>
|
7
6
|
<a href="https://rubygems.org/gems/attr_keyring"><img src="https://img.shields.io/gem/v/attr_keyring.svg" alt="Gem"></a>
|
8
7
|
<a href="https://rubygems.org/gems/attr_keyring"><img src="https://img.shields.io/gem/dt/attr_keyring.svg" alt="Gem"></a>
|
9
8
|
</p>
|
10
9
|
|
11
|
-
N.B.: attr_keyring is
|
10
|
+
N.B.: attr_keyring is not for encrypting passwords--for that, you should use
|
11
|
+
something like [bcrypt](https://github.com/codahale/bcrypt-ruby). It's meant for
|
12
|
+
encrypting sensitive data you will need to access in plain text (e.g. storing
|
13
|
+
OAuth token from users). Passwords do not fall in that category.
|
12
14
|
|
13
|
-
This library is heavily inspired by
|
15
|
+
This library is heavily inspired by
|
16
|
+
[attr_vault](https://github.com/uhoh-itsmaciek/attr_vault), and can read
|
17
|
+
encrypted messages if you encode them in base64 (e.g.
|
18
|
+
`Base64.strict_encode64(encrypted_by_attr_vault)`).
|
14
19
|
|
15
20
|
## Installation
|
16
21
|
|
@@ -36,7 +41,10 @@ Or install it yourself as:
|
|
36
41
|
gem "attr_keyring"
|
37
42
|
require "keyring"
|
38
43
|
|
39
|
-
keyring = Keyring.new(
|
44
|
+
keyring = Keyring.new(
|
45
|
+
{"1" => "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M="},
|
46
|
+
digest_salt: "<custom salt>"
|
47
|
+
)
|
40
48
|
|
41
49
|
# STEP 1: Encrypt message using latest encryption key.
|
42
50
|
encrypted, keyring_id, digest = keyring.encrypt("super secret")
|
@@ -52,30 +60,37 @@ puts "✉️ #{decrypted}"
|
|
52
60
|
|
53
61
|
#### Change encryption algorithm
|
54
62
|
|
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`.
|
63
|
+
You can choose between `AES-128-CBC`, `AES-192-CBC` and `AES-256-CBC`. By
|
64
|
+
default, `AES-128-CBC` will be used.
|
58
65
|
|
59
|
-
|
60
|
-
|
66
|
+
To specify the encryption algorithm, set the `encryption` option. The following
|
67
|
+
example uses `AES-256-CBC`.
|
61
68
|
|
62
|
-
|
63
|
-
|
69
|
+
```ruby
|
70
|
+
keyring = Keyring.new(
|
71
|
+
"1" => "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M=",
|
72
|
+
encryptor: Keyring::Encryptor::AES::AES256CBC,
|
73
|
+
digest_salt: "<custom salt>"
|
74
|
+
)
|
64
75
|
```
|
65
76
|
|
66
77
|
### Configuration
|
67
78
|
|
68
79
|
As far as database schema goes:
|
69
80
|
|
70
|
-
1. You'll need a column to track the key that was used for encryption; by
|
71
|
-
|
72
|
-
|
81
|
+
1. You'll need a column to track the key that was used for encryption; by
|
82
|
+
default it's called `keyring_id`.
|
83
|
+
2. Every encrypted column must follow the name `encrypted_<column name>`.
|
84
|
+
3. Optionally, you can also have a `<column name>_digest` to help with searching
|
85
|
+
(see Lookup section below).
|
73
86
|
|
74
|
-
As far as model configuration goes, they're pretty similar, as you can see
|
87
|
+
As far as model configuration goes, they're pretty similar, as you can see
|
88
|
+
below:
|
75
89
|
|
76
90
|
#### ActiveRecord
|
77
91
|
|
78
|
-
From Rails 5+, ActiveRecord models now inherit from `ApplicationRecord` instead.
|
92
|
+
From Rails 5+, ActiveRecord models now inherit from `ApplicationRecord` instead.
|
93
|
+
This is how you set it up:
|
79
94
|
|
80
95
|
```ruby
|
81
96
|
class ApplicationRecord < ActiveRecord::Base
|
@@ -86,7 +101,8 @@ end
|
|
86
101
|
|
87
102
|
#### Sequel
|
88
103
|
|
89
|
-
Sequel doesn't have an abstract model class (but it could), so you can set up
|
104
|
+
Sequel doesn't have an abstract model class (but it could), so you can set up
|
105
|
+
the model class directly like the following:
|
90
106
|
|
91
107
|
```ruby
|
92
108
|
class User < Sequel::Model
|
@@ -96,16 +112,20 @@ end
|
|
96
112
|
|
97
113
|
### Defining encrypted attributes
|
98
114
|
|
99
|
-
To set up your model, you have to define the keyring (set of encryption keys)
|
115
|
+
To set up your model, you have to define the keyring (set of encryption keys)
|
116
|
+
and the attributes that will be encrypted. Both ActiveRecord and Sequel have the
|
117
|
+
same API, so the examples below work for both ORMs.
|
100
118
|
|
101
119
|
```ruby
|
102
120
|
class User < ApplicationRecord
|
103
|
-
attr_keyring ENV["USER_KEYRING"]
|
121
|
+
attr_keyring ENV["USER_KEYRING"],
|
122
|
+
digest_salt: "<custom salt>"
|
104
123
|
attr_encrypt :twitter_oauth_token, :social_security_number
|
105
124
|
end
|
106
125
|
```
|
107
126
|
|
108
|
-
The code above will encrypt your columns with the current key. If you're
|
127
|
+
The code above will encrypt your columns with the current key. If you're
|
128
|
+
updating a record, then the column will be migrated to the latest key available.
|
109
129
|
|
110
130
|
You can use the model as you would normally do.
|
111
131
|
|
@@ -124,22 +144,44 @@ user.encrypted_email
|
|
124
144
|
#=> WG8Epo0ABz0Z1X5gX7kttc98w9Ei59B5uXGK36Zin9G0VqbxX3naOWOm4RI6w6Uu
|
125
145
|
```
|
126
146
|
|
147
|
+
If you want to store a hash, you can use the `encoder:` option.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class User < ApplicationRecord
|
151
|
+
attr_keyring ENV["USER_KEYRING"],
|
152
|
+
digest_salt: "<custom salt>"
|
153
|
+
|
154
|
+
attr_encrypt :data, encoder: JSON
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
158
|
+
An encoder is just an object that responds to the methods `dump(data)` and
|
159
|
+
`parse(data)`, just like the `JSON` interface. Alternatively, you can use
|
160
|
+
`AttrKeyring::Encoders::JSON`, which returns hashes with symbolized keys.
|
161
|
+
|
127
162
|
### Encryption
|
128
163
|
|
129
|
-
By default, AES-128-CBC is the algorithm used for encryption. This algorithm
|
164
|
+
By default, AES-128-CBC is the algorithm used for encryption. This algorithm
|
165
|
+
uses 16 bytes keys, but you're required to use a key that's double the size
|
166
|
+
because half of that keys will be used to generate the HMAC. The first 16 bytes
|
167
|
+
will be used as the encryption key, and the last 16 bytes will be used to
|
168
|
+
generate the HMAC.
|
130
169
|
|
131
|
-
Using random data base64-encoded is the recommended way. You can easily generate
|
170
|
+
Using random data base64-encoded is the recommended way. You can easily generate
|
171
|
+
keys by using the following command:
|
132
172
|
|
133
173
|
```console
|
134
174
|
$ dd if=/dev/urandom bs=32 count=1 2>/dev/null | openssl base64 -A
|
135
175
|
qUjOJFgZsZbTICsN0TMkKqUvSgObYxnkHDsazTqE5tM=
|
136
176
|
```
|
137
177
|
|
138
|
-
Include the result of this command in the `value` section of the key description
|
178
|
+
Include the result of this command in the `value` section of the key description
|
179
|
+
in the keyring. Half this key is used for encryption, and half for the HMAC.
|
139
180
|
|
140
181
|
#### Key size
|
141
182
|
|
142
|
-
The key size depends on the algorithm being used. The key size should be double
|
183
|
+
The key size depends on the algorithm being used. The key size should be double
|
184
|
+
the size as half of it is used for HMAC computation.
|
143
185
|
|
144
186
|
- `aes-128-cbc`: 16 bytes (encryption) + 16 bytes (HMAC).
|
145
187
|
- `aes-192-cbc`: 24 bytes (encryption) + 24 bytes (HMAC).
|
@@ -147,13 +189,25 @@ The key size depends on the algorithm being used. The key size should be double
|
|
147
189
|
|
148
190
|
#### About the encrypted message
|
149
191
|
|
150
|
-
Initialization vectors (IV) should be unpredictable and unique; ideally, they
|
192
|
+
Initialization vectors (IV) should be unpredictable and unique; ideally, they
|
193
|
+
will be cryptographically random. They do not have to be secret: IVs are
|
194
|
+
typically just added to ciphertext messages unencrypted. It may sound
|
195
|
+
contradictory that something has to be unpredictable and unique, but does not
|
196
|
+
have to be secret; it is important to remember that an attacker must not be able
|
197
|
+
to predict ahead of time what a given IV will be.
|
151
198
|
|
152
|
-
With that in mind, _attr_keyring_ uses
|
199
|
+
With that in mind, _attr_keyring_ uses
|
200
|
+
`base64(hmac(unencrypted iv + encrypted message) + unencrypted iv + encrypted message)`
|
201
|
+
as the final message. If you're planning to migrate from other encryption
|
202
|
+
mechanisms or read encrypted values from the database without using
|
203
|
+
_attr_keyring_, make sure you account for this. The HMAC is 32-bytes long and
|
204
|
+
the IV is 16-bytes long.
|
153
205
|
|
154
206
|
### Keyring
|
155
207
|
|
156
|
-
Keys are managed through a keyring--a short JSON document describing your
|
208
|
+
Keys are managed through a keyring--a short JSON document describing your
|
209
|
+
encryption keys. The keyring must be a JSON object mapping numeric ids of the
|
210
|
+
keys to the key values. A keyring must have at least one key. For example:
|
157
211
|
|
158
212
|
```json
|
159
213
|
{
|
@@ -162,24 +216,30 @@ Keys are managed through a keyring--a short JSON document describing your encryp
|
|
162
216
|
}
|
163
217
|
```
|
164
218
|
|
165
|
-
The `id` is used to track which key encrypted which piece of data; a key with a
|
219
|
+
The `id` is used to track which key encrypted which piece of data; a key with a
|
220
|
+
larger id is assumed to be newer. The value is the actual bytes of the
|
221
|
+
encryption key.
|
166
222
|
|
167
223
|
#### Dynamically loading keyring
|
168
224
|
|
169
|
-
If you're using Rails 5.2+, you can use credentials to define your keyring. Your
|
225
|
+
If you're using Rails 5.2+, you can use credentials to define your keyring. Your
|
226
|
+
`credentials.yml` must be define like the following:
|
170
227
|
|
171
228
|
```yaml
|
229
|
+
---
|
172
230
|
user_keyring:
|
173
|
-
1: "QSXyoiRDPoJmfkJUZ4hJeQ=="
|
174
|
-
2: "r6AfOeilPDJomFsiOXLdfQ=="
|
231
|
+
"1": "QSXyoiRDPoJmfkJUZ4hJeQ=="
|
232
|
+
"2": "r6AfOeilPDJomFsiOXLdfQ=="
|
175
233
|
```
|
176
234
|
|
177
|
-
Then you can setup your model by using
|
235
|
+
Then you can setup your model by using
|
236
|
+
`attr_keyring Rails.application.credentials.user_keyring`.
|
178
237
|
|
179
|
-
Other possibilities (e.g. the keyring file is provided by configuration
|
238
|
+
Other possibilities (e.g. the keyring file is provided by configuration
|
239
|
+
management):
|
180
240
|
|
181
|
-
- `attr_keyring YAML.load_file(keyring_file)`
|
182
|
-
- `attr_keyring JSON.parse(File.read(keyring_file))`.
|
241
|
+
- `attr_keyring YAML.load_file(keyring_file), digest_salt: "<custom salt>"`
|
242
|
+
- `attr_keyring JSON.parse(File.read(keyring_file)), digest_salt: "<custom salt>"`.
|
183
243
|
|
184
244
|
### Lookup
|
185
245
|
|
@@ -189,17 +249,25 @@ One tricky aspect of encryption is looking up records by known secret. E.g.,
|
|
189
249
|
User.where(email: "john@example.com")
|
190
250
|
```
|
191
251
|
|
192
|
-
is trivial with plain text fields, but impossible with the model defined as
|
252
|
+
is trivial with plain text fields, but impossible with the model defined as
|
253
|
+
above.
|
193
254
|
|
194
|
-
If a column `<attribute>_digest` exists, then a SHA1 digest from the value will
|
255
|
+
If a column `<attribute>_digest` exists, then a SHA1 digest from the value will
|
256
|
+
be saved. This will allow you to lookup by that value instead and add unique
|
257
|
+
indexes. You don't have to use a hashing salt, but it's highly recommended; this
|
258
|
+
way you can avoid leaking your users' info via rainbow tables.
|
195
259
|
|
196
260
|
```ruby
|
197
|
-
User.where(email:
|
261
|
+
User.where(email: User.keyring.digest("john@example.com")).first
|
198
262
|
```
|
199
263
|
|
200
264
|
### Key Rotation
|
201
265
|
|
202
|
-
Because attr_keyring uses a keyring, with access to multiple keys at once, key
|
266
|
+
Because attr_keyring uses a keyring, with access to multiple keys at once, key
|
267
|
+
rotation is fairly straightforward: if you add a key to the keyring with a
|
268
|
+
higher id than any other key, that key will automatically be used for encryption
|
269
|
+
when records are either created or updated. Any keys that are no longer in use
|
270
|
+
can be safely removed from the keyring.
|
203
271
|
|
204
272
|
To check if an existing key with id `123` is still in use, run:
|
205
273
|
|
@@ -208,7 +276,8 @@ To check if an existing key with id `123` is still in use, run:
|
|
208
276
|
User.where(keyring_id: 123).empty?
|
209
277
|
```
|
210
278
|
|
211
|
-
You may not want to wait for records to be updated (e.g. key leaking). In that
|
279
|
+
You may not want to wait for records to be updated (e.g. key leaking). In that
|
280
|
+
case, you can rollout a key rotation:
|
212
281
|
|
213
282
|
```ruby
|
214
283
|
User.where(keyring_id: 1234).find_each do |user|
|
@@ -218,12 +287,18 @@ end
|
|
218
287
|
|
219
288
|
### What if I don't use ActiveRecord/Sequel?
|
220
289
|
|
221
|
-
You can also leverage the encryption mechanism of `attr_keyring` totally
|
290
|
+
You can also leverage the encryption mechanism of `attr_keyring` totally
|
291
|
+
decoupled from ActiveRecord/Sequel. First, make sure you load `keyring` instead.
|
292
|
+
Then you can create a keyring to encrypt/decrypt strings, without even touching
|
293
|
+
the database.
|
222
294
|
|
223
295
|
```ruby
|
224
296
|
require "keyring"
|
225
297
|
|
226
|
-
keyring = Keyring.new(
|
298
|
+
keyring = Keyring.new(
|
299
|
+
{"1" => "QSXyoiRDPoJmfkJUZ4hJeQ=="},
|
300
|
+
digest_salt: "<custom salt>"
|
301
|
+
)
|
227
302
|
|
228
303
|
encrypted, keyring_id, digest = keyring.encrypt("super secret")
|
229
304
|
|
@@ -244,26 +319,41 @@ puts decrypted
|
|
244
319
|
|
245
320
|
### Exchange data with Node.js
|
246
321
|
|
247
|
-
If you use Node.js, you may be interested in
|
322
|
+
If you use Node.js, you may be interested in
|
323
|
+
<https://github.com/fnando/keyring-node>, which is able to read and write
|
324
|
+
messages using the same format.
|
248
325
|
|
249
326
|
## Development
|
250
327
|
|
251
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
328
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
329
|
+
`rake test` to run the tests. You can also run `bin/console` for an interactive
|
330
|
+
prompt that will allow you to experiment.
|
252
331
|
|
253
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To
|
332
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
333
|
+
release a new version, update the version number in `version.rb`, and then run
|
334
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
335
|
+
git commits and tags, and push the `.gem` file to
|
336
|
+
[rubygems.org](https://rubygems.org).
|
254
337
|
|
255
338
|
## Contributing
|
256
339
|
|
257
|
-
Bug reports and pull requests are welcome on GitHub at
|
340
|
+
Bug reports and pull requests are welcome on GitHub at
|
341
|
+
https://github.com/fnando/attr_keyring. This project is intended to be a safe,
|
342
|
+
welcoming space for collaboration, and contributors are expected to adhere to
|
343
|
+
the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
258
344
|
|
259
345
|
## License
|
260
346
|
|
261
|
-
The gem is available as open source under the terms of the
|
347
|
+
The gem is available as open source under the terms of the
|
348
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
262
349
|
|
263
350
|
## Icon
|
264
351
|
|
265
|
-
Icon made by [Icongeek26](https://www.flaticon.com/authors/icongeek26) from
|
352
|
+
Icon made by [Icongeek26](https://www.flaticon.com/authors/icongeek26) from
|
353
|
+
[Flaticon](https://www.flaticon.com/) is licensed by Creative Commons BY 3.0.
|
266
354
|
|
267
355
|
## Code of Conduct
|
268
356
|
|
269
|
-
Everyone interacting in the attr_keyring project’s codebases, issue trackers,
|
357
|
+
Everyone interacting in the attr_keyring project’s codebases, issue trackers,
|
358
|
+
chat rooms and mailing lists is expected to follow the
|
359
|
+
[code of conduct](https://github.com/fnando/attr_keyring/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
data/attr_keyring.gemspec
CHANGED
@@ -12,15 +12,14 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.description = spec.summary
|
13
13
|
spec.homepage = "https://github.com/fnando/attr_keyring"
|
14
14
|
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
15
16
|
|
16
|
-
|
17
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added
|
18
|
-
# into git.
|
19
|
-
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
17
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
20
18
|
`git ls-files -z`
|
21
19
|
.split("\x0")
|
22
20
|
.reject {|f| f.match(%r{^(test|spec|features)/}) }
|
23
21
|
end
|
22
|
+
|
24
23
|
spec.bindir = "exe"
|
25
24
|
spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
|
26
25
|
spec.require_paths = ["lib"]
|
data/lib/attr_keyring/version.rb
CHANGED
data/lib/attr_keyring.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
module AttrKeyring
|
4
4
|
require "attr_keyring/version"
|
5
5
|
require "keyring"
|
6
|
+
require "attr_keyring/encoders/json_encoder"
|
6
7
|
|
7
8
|
def self.active_record
|
8
9
|
require "attr_keyring/active_record"
|
@@ -20,13 +21,11 @@ module AttrKeyring
|
|
20
21
|
include InstanceMethods
|
21
22
|
|
22
23
|
class << self
|
23
|
-
attr_accessor :encrypted_attributes
|
24
|
-
attr_accessor :keyring
|
25
|
-
attr_accessor :keyring_column_name
|
24
|
+
attr_accessor :encrypted_attributes, :keyring, :keyring_column_name
|
26
25
|
end
|
27
26
|
|
28
|
-
self.encrypted_attributes =
|
29
|
-
self.keyring = Keyring.new({})
|
27
|
+
self.encrypted_attributes = {}
|
28
|
+
self.keyring = Keyring.new({}, digest_salt: "")
|
30
29
|
self.keyring_column_name = :keyring_id
|
31
30
|
end
|
32
31
|
end
|
@@ -40,15 +39,16 @@ module AttrKeyring
|
|
40
39
|
subclass.keyring_column_name = keyring_column_name
|
41
40
|
end
|
42
41
|
|
43
|
-
def attr_keyring(keyring,
|
44
|
-
self.keyring = Keyring.new(keyring,
|
42
|
+
def attr_keyring(keyring, options = {})
|
43
|
+
self.keyring = Keyring.new(keyring, options)
|
45
44
|
end
|
46
45
|
|
47
|
-
def attr_encrypt(*attributes)
|
48
|
-
self.encrypted_attributes ||=
|
49
|
-
encrypted_attributes.push(*attributes)
|
46
|
+
def attr_encrypt(*attributes, encoder: nil)
|
47
|
+
self.encrypted_attributes ||= {}
|
50
48
|
|
51
49
|
attributes.each do |attribute|
|
50
|
+
encrypted_attributes[attribute.to_sym] = {encoder: encoder}
|
51
|
+
|
52
52
|
define_attr_encrypt_writer(attribute)
|
53
53
|
define_attr_encrypt_reader(attribute)
|
54
54
|
end
|
@@ -72,6 +72,8 @@ module AttrKeyring
|
|
72
72
|
clear_decrypted_column_cache(attribute)
|
73
73
|
return reset_encrypted_column(attribute) unless encryptable_value?(value)
|
74
74
|
|
75
|
+
encoder = self.class.encrypted_attributes[attribute][:encoder]
|
76
|
+
value = encoder.dump(value) if encoder
|
75
77
|
value = value.to_s
|
76
78
|
|
77
79
|
previous_keyring_id = public_send(self.class.keyring_column_name)
|
@@ -88,6 +90,7 @@ module AttrKeyring
|
|
88
90
|
|
89
91
|
private def attr_decrypt_column(attribute)
|
90
92
|
cache_name = :"@#{attribute}"
|
93
|
+
|
91
94
|
if instance_variable_defined?(cache_name)
|
92
95
|
return instance_variable_get(cache_name)
|
93
96
|
end
|
@@ -101,6 +104,9 @@ module AttrKeyring
|
|
101
104
|
public_send(self.class.keyring_column_name)
|
102
105
|
)
|
103
106
|
|
107
|
+
encoder = self.class.encrypted_attributes[attribute][:encoder]
|
108
|
+
decrypted_value = encoder.parse(decrypted_value) if encoder
|
109
|
+
|
104
110
|
instance_variable_set(cache_name, decrypted_value)
|
105
111
|
end
|
106
112
|
|
@@ -125,10 +131,13 @@ module AttrKeyring
|
|
125
131
|
|
126
132
|
keyring_id = self.class.keyring.current_key.id
|
127
133
|
|
128
|
-
self.class.encrypted_attributes.each do |attribute|
|
134
|
+
self.class.encrypted_attributes.each do |attribute, options|
|
129
135
|
value = public_send(attribute)
|
130
136
|
next unless encryptable_value?(value)
|
131
137
|
|
138
|
+
encoder = options[:encoder]
|
139
|
+
value = encoder.dump(value) if encoder
|
140
|
+
|
132
141
|
encrypted_value, _, digest = self.class.keyring.encrypt(value)
|
133
142
|
|
134
143
|
public_send("encrypted_#{attribute}=", encrypted_value)
|
@@ -38,7 +38,10 @@ module Keyring
|
|
38
38
|
expected_hmac = hmac_digest(key.signing_key, encrypted_payload)
|
39
39
|
|
40
40
|
unless verify_signature(expected_hmac, hmac)
|
41
|
-
raise InvalidAuthentication,
|
41
|
+
raise InvalidAuthentication,
|
42
|
+
"Expected HMAC to be " \
|
43
|
+
"#{Base64.strict_encode64(expected_hmac)}; " \
|
44
|
+
"got #{Base64.strict_encode64(hmac)} instead"
|
42
45
|
end
|
43
46
|
|
44
47
|
cipher.iv = iv
|
data/lib/keyring/key.rb
CHANGED
@@ -5,7 +5,7 @@ module Keyring
|
|
5
5
|
attr_reader :id, :signing_key, :encryption_key
|
6
6
|
|
7
7
|
def initialize(id, key, key_size)
|
8
|
-
@id = Integer(id)
|
8
|
+
@id = Integer(id.to_s)
|
9
9
|
@key_size = key_size
|
10
10
|
@encryption_key, @signing_key = parse_key(key)
|
11
11
|
end
|
@@ -20,7 +20,7 @@ module Keyring
|
|
20
20
|
secret = decode_key(key, expected_key_size)
|
21
21
|
|
22
22
|
unless secret.bytesize == expected_key_size
|
23
|
-
raise InvalidSecret, "Secret must be #{expected_key_size} bytes, instead got #{secret.bytesize}" # rubocop:disable
|
23
|
+
raise InvalidSecret, "Secret must be #{expected_key_size} bytes, instead got #{secret.bytesize}" # rubocop:disable Layout/LineLength
|
24
24
|
end
|
25
25
|
|
26
26
|
signing_key = secret[0...@key_size]
|
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.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nando Vieira
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-07-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -185,9 +185,11 @@ executables: []
|
|
185
185
|
extensions: []
|
186
186
|
extra_rdoc_files: []
|
187
187
|
files:
|
188
|
+
- ".github/FUNDING.yml"
|
189
|
+
- ".github/dependabot.yml"
|
190
|
+
- ".github/workflows/tests.yml"
|
188
191
|
- ".gitignore"
|
189
192
|
- ".rubocop.yml"
|
190
|
-
- ".travis.yml"
|
191
193
|
- CODE_OF_CONDUCT.md
|
192
194
|
- Gemfile
|
193
195
|
- LICENSE.txt
|
@@ -201,10 +203,12 @@ files:
|
|
201
203
|
- examples/active_record_sample.rb
|
202
204
|
- examples/keyring_sample.rb
|
203
205
|
- examples/sequel_sample.rb
|
204
|
-
- gemfiles/5_2.gemfile
|
205
206
|
- gemfiles/6_0.gemfile
|
207
|
+
- gemfiles/6_1.gemfile
|
208
|
+
- gemfiles/7_0.gemfile
|
206
209
|
- lib/attr_keyring.rb
|
207
210
|
- lib/attr_keyring/active_record.rb
|
211
|
+
- lib/attr_keyring/encoders/json_encoder.rb
|
208
212
|
- lib/attr_keyring/sequel.rb
|
209
213
|
- lib/attr_keyring/version.rb
|
210
214
|
- lib/keyring.rb
|
@@ -214,7 +218,7 @@ homepage: https://github.com/fnando/attr_keyring
|
|
214
218
|
licenses:
|
215
219
|
- MIT
|
216
220
|
metadata: {}
|
217
|
-
post_install_message:
|
221
|
+
post_install_message:
|
218
222
|
rdoc_options: []
|
219
223
|
require_paths:
|
220
224
|
- lib
|
@@ -222,15 +226,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
222
226
|
requirements:
|
223
227
|
- - ">="
|
224
228
|
- !ruby/object:Gem::Version
|
225
|
-
version:
|
229
|
+
version: 2.5.0
|
226
230
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
227
231
|
requirements:
|
228
232
|
- - ">="
|
229
233
|
- !ruby/object:Gem::Version
|
230
234
|
version: '0'
|
231
235
|
requirements: []
|
232
|
-
rubygems_version: 3.
|
233
|
-
signing_key:
|
236
|
+
rubygems_version: 3.3.7
|
237
|
+
signing_key:
|
234
238
|
specification_version: 4
|
235
239
|
summary: Simple encryption-at-rest plugin for ActiveRecord.
|
236
240
|
test_files: []
|
data/.travis.yml
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
---
|
2
|
-
|
3
|
-
language: ruby
|
4
|
-
cache: bundler
|
5
|
-
sudo: false
|
6
|
-
notifications:
|
7
|
-
email: false
|
8
|
-
rvm:
|
9
|
-
- 2.6.5
|
10
|
-
- 2.5.7
|
11
|
-
services:
|
12
|
-
- postgresql
|
13
|
-
gemfiles:
|
14
|
-
- gemfiles/6_0.gemfile
|
15
|
-
- gemfiles/5_2.gemfile
|
16
|
-
before_script:
|
17
|
-
- createdb test
|
18
|
-
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
19
|
-
- chmod +x ./cc-test-reporter
|
20
|
-
- "./cc-test-reporter before-build"
|
21
|
-
after_script:
|
22
|
-
- "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
|
23
|
-
env:
|
24
|
-
global:
|
25
|
-
secure: c0y7opFgX78UQL0dVq2gciMr3Ca4y4Aw4cSbQMnUwGecwuzOPUhjV98yy4b6EpQ0bLVbVcSPtx/PCVV750nxJPQsz9tWS0yGxQPBXuh2w0AX+ErYJVYaF6+hTjovEiHB86Q9g8YCD29CIMLZs2yeUrB+ORJWQcuAn8fw475Zskk8d8BWqR8CDdonFKlwS0Bx6rOqkyVy0JiNbOM4+trV/RzrNC+dc1geqOo45ceTYiGzkkMU1XANjNhzl/v0DYtCWLF/Dj1s8da96btqU6msZDfsBM73zKWtu0KJMnzqa8Ba4Tjc39kd2ro6Zb22cELBdXOFBvNCAEjbmZIaJ2OC45fES1OGZnB66SjAScdVdxKy2jOWjlFvrRiHu3Zrbl5tFTEaJ/PMHueQn4AzneK1wU2kzjq5iCwBZtMp/iJtCvz0V6qBt77qJe65YuENhcj26cDMqQkhKd0QBTWNs8r02KY3HFKcprgM+2TXxVSvfDu2cbiMInvc3K+uFNnEbu/1piTyStKWGd64WHixV6CEFpHxLU04IUNB62mSvUZtZ6V782X9kawoRyUg6lWvXmnGUUvczdJdpSR5/3gVXOWHireYy/qA6Zqoup27PPoaNgnKCa/fWvN/aJDvrGJb9OWpiK8DGi6T35V5gtDF+vd8mVzyPnYJznlWLgA5m7FSzLg=
|