attr_keyring 0.5.2 → 0.6.1
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 +65 -0
- data/.rubocop.yml +13 -64
- data/Gemfile +2 -0
- data/README.md +126 -51
- data/Rakefile +3 -1
- data/attr_keyring.gemspec +9 -4
- data/bin/console +1 -0
- data/examples/active_record_sample.rb +2 -0
- data/examples/keyring_sample.rb +2 -0
- data/examples/sequel_sample.rb +2 -0
- data/gemfiles/6_0.gemfile +5 -0
- data/gemfiles/7_0.gemfile +5 -0
- data/lib/attr_keyring/active_record.rb +2 -0
- data/lib/attr_keyring/sequel.rb +2 -0
- data/lib/attr_keyring/version.rb +3 -1
- data/lib/attr_keyring.rb +33 -13
- data/lib/keyring/encryptor/aes.rb +5 -1
- data/lib/keyring/key.rb +7 -3
- data/lib/keyring.rb +26 -6
- metadata +26 -8
- data/.travis.yml +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d02280da9cb28259980ea283b6030672957657ead8b895594176eef09d78382b
|
4
|
+
data.tar.gz: 88ddda0bb1d9a85869246e46ab49a8f4889bc0c398d616be9dd162eec239a177
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6a3108027fac7dbfee097e7afa3227f58f33560e540f19bc0f0b63cb4e482e8315ffafdbd4360bc31724426c9150109c6b9e46e9ecdf70526ca13cb34a8bee93
|
7
|
+
data.tar.gz: 16a5ca41d03b434dbf2eb4a5751af6595e7d00c93867159c3990a3e1d0a104d170dfd434580eff0420b1b966afe0877ec70362c6d66cad34e89e0f5fed8867d4
|
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,65 @@
|
|
1
|
+
---
|
2
|
+
name: Tests
|
3
|
+
|
4
|
+
on:
|
5
|
+
pull_request:
|
6
|
+
push:
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
build:
|
10
|
+
name: Tests with Ruby ${{ matrix.ruby }} with ${{ matrix.gemfile }}
|
11
|
+
runs-on: "ubuntu-latest"
|
12
|
+
strategy:
|
13
|
+
fail-fast: false
|
14
|
+
matrix:
|
15
|
+
ruby: ["2.7", "3.0"]
|
16
|
+
gemfile:
|
17
|
+
- gemfiles/7_0.gemfile
|
18
|
+
- gemfiles/6_0.gemfile
|
19
|
+
|
20
|
+
services:
|
21
|
+
postgres:
|
22
|
+
image: postgres:11.5
|
23
|
+
ports: ["5432:5432"]
|
24
|
+
options:
|
25
|
+
--health-cmd pg_isready --health-interval 10s --health-timeout 5s
|
26
|
+
--health-retries 5
|
27
|
+
|
28
|
+
steps:
|
29
|
+
- uses: actions/checkout@v1
|
30
|
+
|
31
|
+
- uses: actions/cache@v2
|
32
|
+
with:
|
33
|
+
path: vendor/bundle
|
34
|
+
key: >
|
35
|
+
${{ runner.os }}-${{ matrix.ruby }}-gems-${{
|
36
|
+
hashFiles('**/attr_keyring.gemspec') }}
|
37
|
+
restore-keys: >
|
38
|
+
${{ runner.os }}-${{ matrix.ruby }}-gems-${{
|
39
|
+
hashFiles('**/attr_keyring.gemspec') }}
|
40
|
+
|
41
|
+
- name: Set up Ruby
|
42
|
+
uses: ruby/setup-ruby@v1
|
43
|
+
with:
|
44
|
+
ruby-version: ${{ matrix.ruby }}
|
45
|
+
|
46
|
+
- name: Install PostgreSQL 11 client
|
47
|
+
run: |
|
48
|
+
sudo apt-get -yqq install libpq-dev
|
49
|
+
|
50
|
+
- name: Install gem dependencies
|
51
|
+
env:
|
52
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
53
|
+
run: |
|
54
|
+
gem install bundler
|
55
|
+
bundle config path vendor/bundle
|
56
|
+
bundle update --jobs 4 --retry 3
|
57
|
+
|
58
|
+
- name: Run Tests
|
59
|
+
env:
|
60
|
+
PGHOST: localhost
|
61
|
+
PGUSER: postgres
|
62
|
+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
|
63
|
+
run: |
|
64
|
+
psql -U postgres -c "create database test"
|
65
|
+
bundle exec rake
|
data/.rubocop.yml
CHANGED
@@ -1,73 +1,22 @@
|
|
1
|
+
---
|
2
|
+
inherit_gem:
|
3
|
+
rubocop-fnando: .rubocop.yml
|
4
|
+
|
1
5
|
AllCops:
|
2
6
|
TargetRubyVersion: 2.5
|
3
|
-
|
4
|
-
-
|
5
|
-
-
|
6
|
-
- '**/*.jbuilder'
|
7
|
-
- '**/*.rake'
|
8
|
-
- '**/*.opal'
|
9
|
-
- '**/config.ru'
|
10
|
-
- '**/Gemfile'
|
11
|
-
- '**/Rakefile'
|
12
|
-
- '**/Capfile'
|
13
|
-
- '**/Guardfile'
|
14
|
-
- '**/Podfile'
|
15
|
-
- '**/Thorfile'
|
16
|
-
- '**/Vagrantfile'
|
17
|
-
- '**/Berksfile'
|
18
|
-
- '**/Cheffile'
|
19
|
-
- '**/Vagabondfile'
|
20
|
-
|
21
|
-
Style/Documentation:
|
22
|
-
Enabled: false
|
23
|
-
|
24
|
-
Style/StringLiterals:
|
25
|
-
EnforcedStyle: double_quotes
|
26
|
-
|
27
|
-
Layout/SpaceInsideBlockBraces:
|
28
|
-
EnforcedStyle: space
|
29
|
-
EnforcedStyleForEmptyBraces: space
|
30
|
-
SpaceBeforeBlockParameters: false
|
31
|
-
|
32
|
-
Layout/SpaceInsideHashLiteralBraces:
|
33
|
-
EnforcedStyle: no_space
|
34
|
-
EnforcedStyleForEmptyBraces: no_space
|
7
|
+
Exclude:
|
8
|
+
- vendor/**/*
|
9
|
+
- gemfiles/**/*
|
35
10
|
|
36
|
-
|
37
|
-
Enabled: true
|
38
|
-
|
39
|
-
Layout/FirstHashElementLineBreak:
|
40
|
-
Enabled: true
|
41
|
-
|
42
|
-
Style/SymbolArray:
|
43
|
-
Enabled: true
|
44
|
-
|
45
|
-
Style/PercentLiteralDelimiters:
|
46
|
-
PreferredDelimiters:
|
47
|
-
'%': '[]'
|
48
|
-
'%i': '[]'
|
49
|
-
'%q': '[]'
|
50
|
-
'%Q': '[]'
|
51
|
-
'%r': '{}'
|
52
|
-
'%s': '[]'
|
53
|
-
'%w': '[]'
|
54
|
-
'%W': '[]'
|
55
|
-
'%x': '[]'
|
56
|
-
|
57
|
-
Metrics/LineLength:
|
11
|
+
Metrics/AbcSize:
|
58
12
|
Enabled: false
|
59
13
|
|
60
|
-
|
61
|
-
|
14
|
+
Layout/LineLength:
|
15
|
+
Exclude:
|
16
|
+
- test/**/*
|
62
17
|
|
63
|
-
|
18
|
+
Metrics/MethodLength:
|
64
19
|
Enabled: false
|
65
20
|
|
66
|
-
|
67
|
-
EnforcedStyle: inline
|
68
|
-
|
69
|
-
Style/Alias:
|
70
|
-
EnforcedStyle: prefer_alias_method
|
71
|
-
|
72
|
-
Style/TrailingUnderscoreVariable:
|
21
|
+
Metrics/ClassLength:
|
73
22
|
Enabled: false
|
data/Gemfile
CHANGED
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
|
|
@@ -126,20 +146,27 @@ user.encrypted_email
|
|
126
146
|
|
127
147
|
### Encryption
|
128
148
|
|
129
|
-
By default, AES-128-CBC is the algorithm used for encryption. This algorithm
|
149
|
+
By default, AES-128-CBC is the algorithm used for encryption. This algorithm
|
150
|
+
uses 16 bytes keys, but you're required to use a key that's double the size
|
151
|
+
because half of that keys will be used to generate the HMAC. The first 16 bytes
|
152
|
+
will be used as the encryption key, and the last 16 bytes will be used to
|
153
|
+
generate the HMAC.
|
130
154
|
|
131
|
-
Using random data base64-encoded is the recommended way. You can easily generate
|
155
|
+
Using random data base64-encoded is the recommended way. You can easily generate
|
156
|
+
keys by using the following command:
|
132
157
|
|
133
158
|
```console
|
134
159
|
$ dd if=/dev/urandom bs=32 count=1 2>/dev/null | openssl base64 -A
|
135
160
|
qUjOJFgZsZbTICsN0TMkKqUvSgObYxnkHDsazTqE5tM=
|
136
161
|
```
|
137
162
|
|
138
|
-
Include the result of this command in the `value` section of the key description
|
163
|
+
Include the result of this command in the `value` section of the key description
|
164
|
+
in the keyring. Half this key is used for encryption, and half for the HMAC.
|
139
165
|
|
140
166
|
#### Key size
|
141
167
|
|
142
|
-
The key size depends on the algorithm being used. The key size should be double
|
168
|
+
The key size depends on the algorithm being used. The key size should be double
|
169
|
+
the size as half of it is used for HMAC computation.
|
143
170
|
|
144
171
|
- `aes-128-cbc`: 16 bytes (encryption) + 16 bytes (HMAC).
|
145
172
|
- `aes-192-cbc`: 24 bytes (encryption) + 24 bytes (HMAC).
|
@@ -147,13 +174,25 @@ The key size depends on the algorithm being used. The key size should be double
|
|
147
174
|
|
148
175
|
#### About the encrypted message
|
149
176
|
|
150
|
-
Initialization vectors (IV) should be unpredictable and unique; ideally, they
|
177
|
+
Initialization vectors (IV) should be unpredictable and unique; ideally, they
|
178
|
+
will be cryptographically random. They do not have to be secret: IVs are
|
179
|
+
typically just added to ciphertext messages unencrypted. It may sound
|
180
|
+
contradictory that something has to be unpredictable and unique, but does not
|
181
|
+
have to be secret; it is important to remember that an attacker must not be able
|
182
|
+
to predict ahead of time what a given IV will be.
|
151
183
|
|
152
|
-
With that in mind, _attr_keyring_ uses
|
184
|
+
With that in mind, _attr_keyring_ uses
|
185
|
+
`base64(hmac(unencrypted iv + encrypted message) + unencrypted iv + encrypted message)`
|
186
|
+
as the final message. If you're planning to migrate from other encryption
|
187
|
+
mechanisms or read encrypted values from the database without using
|
188
|
+
_attr_keyring_, make sure you account for this. The HMAC is 32-bytes long and
|
189
|
+
the IV is 16-bytes long.
|
153
190
|
|
154
191
|
### Keyring
|
155
192
|
|
156
|
-
Keys are managed through a keyring--a short JSON document describing your
|
193
|
+
Keys are managed through a keyring--a short JSON document describing your
|
194
|
+
encryption keys. The keyring must be a JSON object mapping numeric ids of the
|
195
|
+
keys to the key values. A keyring must have at least one key. For example:
|
157
196
|
|
158
197
|
```json
|
159
198
|
{
|
@@ -162,24 +201,30 @@ Keys are managed through a keyring--a short JSON document describing your encryp
|
|
162
201
|
}
|
163
202
|
```
|
164
203
|
|
165
|
-
The `id` is used to track which key encrypted which piece of data; a key with a
|
204
|
+
The `id` is used to track which key encrypted which piece of data; a key with a
|
205
|
+
larger id is assumed to be newer. The value is the actual bytes of the
|
206
|
+
encryption key.
|
166
207
|
|
167
208
|
#### Dynamically loading keyring
|
168
209
|
|
169
|
-
If you're using Rails 5.2+, you can use credentials to define your keyring. Your
|
210
|
+
If you're using Rails 5.2+, you can use credentials to define your keyring. Your
|
211
|
+
`credentials.yml` must be define like the following:
|
170
212
|
|
171
213
|
```yaml
|
214
|
+
---
|
172
215
|
user_keyring:
|
173
|
-
1: "QSXyoiRDPoJmfkJUZ4hJeQ=="
|
174
|
-
2: "r6AfOeilPDJomFsiOXLdfQ=="
|
216
|
+
"1": "QSXyoiRDPoJmfkJUZ4hJeQ=="
|
217
|
+
"2": "r6AfOeilPDJomFsiOXLdfQ=="
|
175
218
|
```
|
176
219
|
|
177
|
-
Then you can setup your model by using
|
220
|
+
Then you can setup your model by using
|
221
|
+
`attr_keyring Rails.application.credentials.user_keyring`.
|
178
222
|
|
179
|
-
Other possibilities (e.g. the keyring file is provided by configuration
|
223
|
+
Other possibilities (e.g. the keyring file is provided by configuration
|
224
|
+
management):
|
180
225
|
|
181
|
-
- `attr_keyring YAML.load_file(keyring_file)`
|
182
|
-
- `attr_keyring JSON.parse(File.read(keyring_file))`.
|
226
|
+
- `attr_keyring YAML.load_file(keyring_file), digest_salt: "<custom salt>"`
|
227
|
+
- `attr_keyring JSON.parse(File.read(keyring_file)), digest_salt: "<custom salt>"`.
|
183
228
|
|
184
229
|
### Lookup
|
185
230
|
|
@@ -189,17 +234,25 @@ One tricky aspect of encryption is looking up records by known secret. E.g.,
|
|
189
234
|
User.where(email: "john@example.com")
|
190
235
|
```
|
191
236
|
|
192
|
-
is trivial with plain text fields, but impossible with the model defined as
|
237
|
+
is trivial with plain text fields, but impossible with the model defined as
|
238
|
+
above.
|
193
239
|
|
194
|
-
If a column `<attribute>_digest` exists, then a SHA1 digest from the value will
|
240
|
+
If a column `<attribute>_digest` exists, then a SHA1 digest from the value will
|
241
|
+
be saved. This will allow you to lookup by that value instead and add unique
|
242
|
+
indexes. You don't have to use a hashing salt, but it's highly recommended; this
|
243
|
+
way you can avoid leaking your users' info via rainbow tables.
|
195
244
|
|
196
245
|
```ruby
|
197
|
-
User.where(email:
|
246
|
+
User.where(email: User.keyring.digest("john@example.com")).first
|
198
247
|
```
|
199
248
|
|
200
249
|
### Key Rotation
|
201
250
|
|
202
|
-
Because attr_keyring uses a keyring, with access to multiple keys at once, key
|
251
|
+
Because attr_keyring uses a keyring, with access to multiple keys at once, key
|
252
|
+
rotation is fairly straightforward: if you add a key to the keyring with a
|
253
|
+
higher id than any other key, that key will automatically be used for encryption
|
254
|
+
when records are either created or updated. Any keys that are no longer in use
|
255
|
+
can be safely removed from the keyring.
|
203
256
|
|
204
257
|
To check if an existing key with id `123` is still in use, run:
|
205
258
|
|
@@ -208,7 +261,8 @@ To check if an existing key with id `123` is still in use, run:
|
|
208
261
|
User.where(keyring_id: 123).empty?
|
209
262
|
```
|
210
263
|
|
211
|
-
You may not want to wait for records to be updated (e.g. key leaking). In that
|
264
|
+
You may not want to wait for records to be updated (e.g. key leaking). In that
|
265
|
+
case, you can rollout a key rotation:
|
212
266
|
|
213
267
|
```ruby
|
214
268
|
User.where(keyring_id: 1234).find_each do |user|
|
@@ -218,12 +272,18 @@ end
|
|
218
272
|
|
219
273
|
### What if I don't use ActiveRecord/Sequel?
|
220
274
|
|
221
|
-
You can also leverage the encryption mechanism of `attr_keyring` totally
|
275
|
+
You can also leverage the encryption mechanism of `attr_keyring` totally
|
276
|
+
decoupled from ActiveRecord/Sequel. First, make sure you load `keyring` instead.
|
277
|
+
Then you can create a keyring to encrypt/decrypt strings, without even touching
|
278
|
+
the database.
|
222
279
|
|
223
280
|
```ruby
|
224
281
|
require "keyring"
|
225
282
|
|
226
|
-
keyring = Keyring.new(
|
283
|
+
keyring = Keyring.new(
|
284
|
+
{"1" => "QSXyoiRDPoJmfkJUZ4hJeQ=="},
|
285
|
+
digest_salt: "<custom salt>"
|
286
|
+
)
|
227
287
|
|
228
288
|
encrypted, keyring_id, digest = keyring.encrypt("super secret")
|
229
289
|
|
@@ -244,26 +304,41 @@ puts decrypted
|
|
244
304
|
|
245
305
|
### Exchange data with Node.js
|
246
306
|
|
247
|
-
If you use Node.js, you may be interested in
|
307
|
+
If you use Node.js, you may be interested in
|
308
|
+
<https://github.com/fnando/keyring-node>, which is able to read and write
|
309
|
+
messages using the same format.
|
248
310
|
|
249
311
|
## Development
|
250
312
|
|
251
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
313
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
314
|
+
`rake test` to run the tests. You can also run `bin/console` for an interactive
|
315
|
+
prompt that will allow you to experiment.
|
252
316
|
|
253
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To
|
317
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
318
|
+
release a new version, update the version number in `version.rb`, and then run
|
319
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
320
|
+
git commits and tags, and push the `.gem` file to
|
321
|
+
[rubygems.org](https://rubygems.org).
|
254
322
|
|
255
323
|
## Contributing
|
256
324
|
|
257
|
-
Bug reports and pull requests are welcome on GitHub at
|
325
|
+
Bug reports and pull requests are welcome on GitHub at
|
326
|
+
https://github.com/fnando/attr_keyring. This project is intended to be a safe,
|
327
|
+
welcoming space for collaboration, and contributors are expected to adhere to
|
328
|
+
the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
258
329
|
|
259
330
|
## License
|
260
331
|
|
261
|
-
The gem is available as open source under the terms of the
|
332
|
+
The gem is available as open source under the terms of the
|
333
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
262
334
|
|
263
335
|
## Icon
|
264
336
|
|
265
|
-
Icon made by [Icongeek26](https://www.flaticon.com/authors/icongeek26) from
|
337
|
+
Icon made by [Icongeek26](https://www.flaticon.com/authors/icongeek26) from
|
338
|
+
[Flaticon](https://www.flaticon.com/) is licensed by Creative Commons BY 3.0.
|
266
339
|
|
267
340
|
## Code of Conduct
|
268
341
|
|
269
|
-
Everyone interacting in the attr_keyring project’s codebases, issue trackers,
|
342
|
+
Everyone interacting in the attr_keyring project’s codebases, issue trackers,
|
343
|
+
chat rooms and mailing lists is expected to follow the
|
344
|
+
[code of conduct](https://github.com/fnando/attr_keyring/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "bundler/gem_tasks"
|
2
4
|
require "rake/testtask"
|
3
5
|
|
@@ -14,4 +16,4 @@ task(:rubocop) do
|
|
14
16
|
RuboCop::CLI.new.run(["--config", File.join(__dir__, ".rubocop.yml")])
|
15
17
|
end
|
16
18
|
|
17
|
-
task :
|
19
|
+
task default: %i[test rubocop]
|
data/attr_keyring.gemspec
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "./lib/attr_keyring/version"
|
2
4
|
|
3
5
|
Gem::Specification.new do |spec|
|
@@ -10,12 +12,14 @@ Gem::Specification.new do |spec|
|
|
10
12
|
spec.description = spec.summary
|
11
13
|
spec.homepage = "https://github.com/fnando/attr_keyring"
|
12
14
|
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
13
16
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
18
|
+
`git ls-files -z`
|
19
|
+
.split("\x0")
|
20
|
+
.reject {|f| f.match(%r{^(test|spec|features)/}) }
|
18
21
|
end
|
22
|
+
|
19
23
|
spec.bindir = "exe"
|
20
24
|
spec.executables = spec.files.grep(%r{^exe/}) {|f| File.basename(f) }
|
21
25
|
spec.require_paths = ["lib"]
|
@@ -29,6 +33,7 @@ Gem::Specification.new do |spec|
|
|
29
33
|
spec.add_development_dependency "pry-meta"
|
30
34
|
spec.add_development_dependency "rake"
|
31
35
|
spec.add_development_dependency "rubocop"
|
36
|
+
spec.add_development_dependency "rubocop-fnando"
|
32
37
|
spec.add_development_dependency "sequel"
|
33
38
|
spec.add_development_dependency "simplecov"
|
34
39
|
end
|
data/bin/console
CHANGED
data/examples/keyring_sample.rb
CHANGED
data/examples/sequel_sample.rb
CHANGED
data/lib/attr_keyring/sequel.rb
CHANGED
data/lib/attr_keyring/version.rb
CHANGED
data/lib/attr_keyring.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module AttrKeyring
|
2
4
|
require "attr_keyring/version"
|
3
5
|
require "keyring"
|
@@ -18,13 +20,11 @@ module AttrKeyring
|
|
18
20
|
include InstanceMethods
|
19
21
|
|
20
22
|
class << self
|
21
|
-
attr_accessor :encrypted_attributes
|
22
|
-
attr_accessor :keyring
|
23
|
-
attr_accessor :keyring_column_name
|
23
|
+
attr_accessor :encrypted_attributes, :keyring, :keyring_column_name
|
24
24
|
end
|
25
25
|
|
26
26
|
self.encrypted_attributes = []
|
27
|
-
self.keyring = Keyring.new({})
|
27
|
+
self.keyring = Keyring.new({}, digest_salt: "")
|
28
28
|
self.keyring_column_name = :keyring_id
|
29
29
|
end
|
30
30
|
end
|
@@ -38,8 +38,8 @@ module AttrKeyring
|
|
38
38
|
subclass.keyring_column_name = keyring_column_name
|
39
39
|
end
|
40
40
|
|
41
|
-
def attr_keyring(keyring,
|
42
|
-
self.keyring = Keyring.new(keyring,
|
41
|
+
def attr_keyring(keyring, options = {})
|
42
|
+
self.keyring = Keyring.new(keyring, options)
|
43
43
|
end
|
44
44
|
|
45
45
|
def attr_encrypt(*attributes)
|
@@ -73,37 +73,54 @@ module AttrKeyring
|
|
73
73
|
value = value.to_s
|
74
74
|
|
75
75
|
previous_keyring_id = public_send(self.class.keyring_column_name)
|
76
|
-
encrypted_value, keyring_id, digest =
|
76
|
+
encrypted_value, keyring_id, digest =
|
77
|
+
self.class.keyring.encrypt(value, previous_keyring_id)
|
77
78
|
|
78
79
|
public_send("#{self.class.keyring_column_name}=", keyring_id)
|
79
80
|
public_send("encrypted_#{attribute}=", encrypted_value)
|
80
|
-
|
81
|
+
|
82
|
+
return unless respond_to?("#{attribute}_digest=")
|
83
|
+
|
84
|
+
public_send("#{attribute}_digest=", digest)
|
81
85
|
end
|
82
86
|
|
83
87
|
private def attr_decrypt_column(attribute)
|
84
88
|
cache_name = :"@#{attribute}"
|
85
|
-
|
89
|
+
if instance_variable_defined?(cache_name)
|
90
|
+
return instance_variable_get(cache_name)
|
91
|
+
end
|
86
92
|
|
87
93
|
encrypted_value = public_send("encrypted_#{attribute}")
|
94
|
+
|
88
95
|
return unless encrypted_value
|
89
96
|
|
90
|
-
decrypted_value = self.class.keyring.decrypt(
|
97
|
+
decrypted_value = self.class.keyring.decrypt(
|
98
|
+
encrypted_value,
|
99
|
+
public_send(self.class.keyring_column_name)
|
100
|
+
)
|
91
101
|
|
92
102
|
instance_variable_set(cache_name, decrypted_value)
|
93
103
|
end
|
94
104
|
|
95
105
|
private def clear_decrypted_column_cache(attribute)
|
96
106
|
cache_name = :"@#{attribute}"
|
97
|
-
|
107
|
+
|
108
|
+
return unless instance_variable_defined?(cache_name)
|
109
|
+
|
110
|
+
remove_instance_variable(cache_name)
|
98
111
|
end
|
99
112
|
|
100
113
|
private def reset_encrypted_column(attribute)
|
101
114
|
public_send("encrypted_#{attribute}=", nil)
|
102
|
-
|
115
|
+
if respond_to?("#{attribute}_digest=")
|
116
|
+
public_send("#{attribute}_digest=", nil)
|
117
|
+
end
|
103
118
|
nil
|
104
119
|
end
|
105
120
|
|
106
121
|
private def migrate_to_latest_encryption_key
|
122
|
+
return unless self.class.keyring.current_key
|
123
|
+
|
107
124
|
keyring_id = self.class.keyring.current_key.id
|
108
125
|
|
109
126
|
self.class.encrypted_attributes.each do |attribute|
|
@@ -113,7 +130,10 @@ module AttrKeyring
|
|
113
130
|
encrypted_value, _, digest = self.class.keyring.encrypt(value)
|
114
131
|
|
115
132
|
public_send("encrypted_#{attribute}=", encrypted_value)
|
116
|
-
|
133
|
+
|
134
|
+
if respond_to?("#{attribute}_digest")
|
135
|
+
public_send("#{attribute}_digest=", digest)
|
136
|
+
end
|
117
137
|
end
|
118
138
|
|
119
139
|
public_send("#{self.class.keyring_column_name}=", keyring_id)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Keyring
|
2
4
|
module Encryptor
|
3
5
|
module AES
|
@@ -35,7 +37,9 @@ module Keyring
|
|
35
37
|
|
36
38
|
expected_hmac = hmac_digest(key.signing_key, encrypted_payload)
|
37
39
|
|
38
|
-
|
40
|
+
unless verify_signature(expected_hmac, hmac)
|
41
|
+
raise InvalidAuthentication, "Expected HMAC to be #{Base64.strict_encode64(expected_hmac)}; got #{Base64.strict_encode64(hmac)} instead" # rubocop:disable Layout/LineLength
|
42
|
+
end
|
39
43
|
|
40
44
|
cipher.iv = iv
|
41
45
|
cipher.key = key.encryption_key
|
data/lib/keyring/key.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Keyring
|
2
4
|
class Key
|
3
5
|
attr_reader :id, :signing_key, :encryption_key
|
4
6
|
|
5
7
|
def initialize(id, key, key_size)
|
6
|
-
@id = Integer(id)
|
8
|
+
@id = Integer(id.to_s)
|
7
9
|
@key_size = key_size
|
8
10
|
@encryption_key, @signing_key = parse_key(key)
|
9
11
|
end
|
@@ -11,13 +13,15 @@ module Keyring
|
|
11
13
|
def to_s
|
12
14
|
"#<Keyring::Key id=#{id.inspect}>"
|
13
15
|
end
|
14
|
-
|
16
|
+
alias inspect to_s
|
15
17
|
|
16
18
|
private def parse_key(key)
|
17
19
|
expected_key_size = @key_size * 2
|
18
20
|
secret = decode_key(key, expected_key_size)
|
19
21
|
|
20
|
-
|
22
|
+
unless secret.bytesize == expected_key_size
|
23
|
+
raise InvalidSecret, "Secret must be #{expected_key_size} bytes, instead got #{secret.bytesize}" # rubocop:disable Layout/LineLength
|
24
|
+
end
|
21
25
|
|
22
26
|
signing_key = secret[0...@key_size]
|
23
27
|
encryption_key = secret[@key_size..-1]
|
data/lib/keyring.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Keyring
|
2
4
|
require "openssl"
|
3
5
|
require "base64"
|
@@ -10,10 +12,19 @@ module Keyring
|
|
10
12
|
InvalidSecret = Class.new(StandardError)
|
11
13
|
EmptyKeyring = Class.new(StandardError)
|
12
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
|
13
23
|
|
14
24
|
class Base
|
15
|
-
def initialize(keyring,
|
16
|
-
@encryptor = encryptor
|
25
|
+
def initialize(keyring, options)
|
26
|
+
@encryptor = options[:encryptor]
|
27
|
+
@digest_salt = options[:digest_salt]
|
17
28
|
@keyring = keyring.map do |id, value|
|
18
29
|
Key.new(id, value, @encryptor.key_size)
|
19
30
|
end
|
@@ -42,13 +53,12 @@ module Keyring
|
|
42
53
|
|
43
54
|
def encrypt(message, keyring_id = nil)
|
44
55
|
keyring_id ||= current_key&.id
|
45
|
-
digest = Digest::SHA1.hexdigest(message)
|
46
56
|
key = self[keyring_id]
|
47
57
|
|
48
58
|
[
|
49
59
|
@encryptor.encrypt(key, message),
|
50
60
|
keyring_id,
|
51
|
-
digest
|
61
|
+
digest(message)
|
52
62
|
]
|
53
63
|
end
|
54
64
|
|
@@ -56,9 +66,19 @@ module Keyring
|
|
56
66
|
key = self[keyring_id]
|
57
67
|
@encryptor.decrypt(key, message)
|
58
68
|
end
|
69
|
+
|
70
|
+
def digest(message)
|
71
|
+
Digest::SHA1.hexdigest("#{message}#{@digest_salt}")
|
72
|
+
end
|
59
73
|
end
|
60
74
|
|
61
|
-
def self.new(keyring,
|
62
|
-
|
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)
|
63
83
|
end
|
64
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.1
|
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: 2021-12-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -136,6 +136,20 @@ dependencies:
|
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop-fnando
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
139
153
|
- !ruby/object:Gem::Dependency
|
140
154
|
name: sequel
|
141
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -171,9 +185,11 @@ executables: []
|
|
171
185
|
extensions: []
|
172
186
|
extra_rdoc_files: []
|
173
187
|
files:
|
188
|
+
- ".github/FUNDING.yml"
|
189
|
+
- ".github/dependabot.yml"
|
190
|
+
- ".github/workflows/tests.yml"
|
174
191
|
- ".gitignore"
|
175
192
|
- ".rubocop.yml"
|
176
|
-
- ".travis.yml"
|
177
193
|
- CODE_OF_CONDUCT.md
|
178
194
|
- Gemfile
|
179
195
|
- LICENSE.txt
|
@@ -187,6 +203,8 @@ files:
|
|
187
203
|
- examples/active_record_sample.rb
|
188
204
|
- examples/keyring_sample.rb
|
189
205
|
- examples/sequel_sample.rb
|
206
|
+
- gemfiles/6_0.gemfile
|
207
|
+
- gemfiles/7_0.gemfile
|
190
208
|
- lib/attr_keyring.rb
|
191
209
|
- lib/attr_keyring/active_record.rb
|
192
210
|
- lib/attr_keyring/sequel.rb
|
@@ -198,7 +216,7 @@ homepage: https://github.com/fnando/attr_keyring
|
|
198
216
|
licenses:
|
199
217
|
- MIT
|
200
218
|
metadata: {}
|
201
|
-
post_install_message:
|
219
|
+
post_install_message:
|
202
220
|
rdoc_options: []
|
203
221
|
require_paths:
|
204
222
|
- lib
|
@@ -206,15 +224,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
206
224
|
requirements:
|
207
225
|
- - ">="
|
208
226
|
- !ruby/object:Gem::Version
|
209
|
-
version:
|
227
|
+
version: 2.5.0
|
210
228
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
211
229
|
requirements:
|
212
230
|
- - ">="
|
213
231
|
- !ruby/object:Gem::Version
|
214
232
|
version: '0'
|
215
233
|
requirements: []
|
216
|
-
rubygems_version: 3.
|
217
|
-
signing_key:
|
234
|
+
rubygems_version: 3.2.32
|
235
|
+
signing_key:
|
218
236
|
specification_version: 4
|
219
237
|
summary: Simple encryption-at-rest plugin for ActiveRecord.
|
220
238
|
test_files: []
|
data/.travis.yml
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
language: ruby
|
2
|
-
cache: bundler
|
3
|
-
sudo: false
|
4
|
-
notifications:
|
5
|
-
email: false
|
6
|
-
rvm:
|
7
|
-
- 2.5.3
|
8
|
-
before_script:
|
9
|
-
- createdb test
|
10
|
-
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
11
|
-
- chmod +x ./cc-test-reporter
|
12
|
-
- "./cc-test-reporter before-build"
|
13
|
-
after_script:
|
14
|
-
- "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
|
15
|
-
env:
|
16
|
-
global:
|
17
|
-
secure: c0y7opFgX78UQL0dVq2gciMr3Ca4y4Aw4cSbQMnUwGecwuzOPUhjV98yy4b6EpQ0bLVbVcSPtx/PCVV750nxJPQsz9tWS0yGxQPBXuh2w0AX+ErYJVYaF6+hTjovEiHB86Q9g8YCD29CIMLZs2yeUrB+ORJWQcuAn8fw475Zskk8d8BWqR8CDdonFKlwS0Bx6rOqkyVy0JiNbOM4+trV/RzrNC+dc1geqOo45ceTYiGzkkMU1XANjNhzl/v0DYtCWLF/Dj1s8da96btqU6msZDfsBM73zKWtu0KJMnzqa8Ba4Tjc39kd2ro6Zb22cELBdXOFBvNCAEjbmZIaJ2OC45fES1OGZnB66SjAScdVdxKy2jOWjlFvrRiHu3Zrbl5tFTEaJ/PMHueQn4AzneK1wU2kzjq5iCwBZtMp/iJtCvz0V6qBt77qJe65YuENhcj26cDMqQkhKd0QBTWNs8r02KY3HFKcprgM+2TXxVSvfDu2cbiMInvc3K+uFNnEbu/1piTyStKWGd64WHixV6CEFpHxLU04IUNB62mSvUZtZ6V782X9kawoRyUg6lWvXmnGUUvczdJdpSR5/3gVXOWHireYy/qA6Zqoup27PPoaNgnKCa/fWvN/aJDvrGJb9OWpiK8DGi6T35V5gtDF+vd8mVzyPnYJznlWLgA5m7FSzLg=
|