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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2af6cd51b5b53137780ac69ddf709a3e0b6f31c4381e0425be63579fce0be85d
4
- data.tar.gz: 6c3ac6b0b56ecceeecce9e098d8424f5659d8c5d75d26201f87cc669accfe478
3
+ metadata.gz: 4a1335e867e0b79f0f8082b6cceaa30e40feeb7a9cb140dbb133989ad47af66f
4
+ data.tar.gz: fa5803034ce08ff55f515fc87e774eeb16b96597e229f55266bfdafd51225dd0
5
5
  SHA512:
6
- metadata.gz: 151441f7e84eb4145206b96635b07b2e55f83eabe7f855fe03a0e880d65030c9165a5bf09646066ecfd6ef28e0de6b90be8449831858893cf325b6d8b2068027
7
- data.tar.gz: 4b7bb84692b69a794ac42ca1e5674f6c00659de1c77dcfa0f00111f6d6537719e9cf2ed93122562b79d8f9b8a47ff572b0330410c724065c4d5cfab22d8efa28
6
+ metadata.gz: 1d2bc02c88a3871191cc41e32707452f1df9c50040991a8b09394d06a7a06d83a84b3574ac2e1e291e41cb8525d190213e31066eb9922440bbbe3426271829eb
7
+ data.tar.gz: '00086d53fd3bd74cc0ab4a1e5b511b02ddb081dc01531df75999461200c15b5e87069e0b38eb62f5c353ed121cb8f4657ec1e130aebfc0d254528a9365896389'
@@ -0,0 +1,3 @@
1
+ ---
2
+ github: [fnando]
3
+ custom: ["https://www.paypal.me/nandovieira/🍕"]
@@ -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
6
+ TargetRubyVersion: 2.5
7
+ Exclude:
8
+ - vendor/**/*
9
+ - gemfiles/**/*
7
10
 
8
11
  Metrics/AbcSize:
9
12
  Enabled: false
10
13
 
11
- Metrics/LineLength:
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/master/attr_keyring.png)
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://travis-ci.org/fnando/attr_keyring"><img src="https://travis-ci.org/fnando/attr_keyring.svg" alt="Travis-CI"></a>
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 *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.
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 [attr_vault](https://github.com/uhoh-itsmaciek/attr_vault), and can read encrypted messages if you encode them in base64 (e.g. `Base64.strict_encode64(encrypted_by_attr_vault)`).
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("1" => "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M=")
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 default, `AES-128-CBC` will be used.
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
- ```js
60
- import { keyring } from "@fnando/keyring";
66
+ To specify the encryption algorithm, set the `encryption` option. The following
67
+ example uses `AES-256-CBC`.
61
68
 
62
- const keys = {"1": "uDiMcWVNTuz//naQ88sOcN+E40CyBRGzGTT7OkoBS6M="};
63
- const encryptor = keyring(keys, {encryption: "aes-256-cbc"});
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 default it's called `keyring_id`.
71
- 2. Every encrypted columns must follow the name `encrypted_<column name>`.
72
- 3. Optionally, you can also have a `<column name>_digest` to help with searching (see Lookup section below).
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 below:
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. This is how you set it up:
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 the model class directly like the following:
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) and the attributes that will be encrypted. Both ActiveRecord and Sequel have the same API, so the examples below work for both ORMs.
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 updating a record, then the column will be migrated to the latest key available.
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 uses 16 bytes keys, but you're required to use a key that's double the size because half of that keys will be used to generate the HMAC. The first 16 bytes will be used as the encryption key, and the last 16 bytes will be used to generate the HMAC.
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 keys by using the following command:
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 in the keyring. Half this key is used for encryption, and half for the HMAC.
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 the size as half of it is used for HMAC computation.
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 will be cryptographically random. They do not have to be secret: IVs are typically just added to ciphertext messages unencrypted. It may sound contradictory that something has to be unpredictable and unique, but does not have to be secret; it is important to remember that an attacker must not be able to predict ahead of time what a given IV will be.
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 `base64(hmac(unencrypted iv + encrypted message) + unencrypted iv + encrypted message)` as the final message. If you're planning to migrate from other encryption mechanisms or read encrypted values from the database without using _attr_keyring_, make sure you account for this. The HMAC is 32-bytes long and the IV is 16-bytes long.
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 encryption keys. The keyring must be a JSON object mapping numeric ids of the keys to the key values. A keyring must have at least one key. For example:
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 larger id is assumed to be newer. The value is the actual bytes of the encryption key.
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 `credentials.yml` must be define like the following:
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 `attr_keyring Rails.application.credentials.user_keyring`.
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 management):
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 above.
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 be saved. This will allow you to lookup by that value instead and add unique indexes.
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: Digest::SHA1.hexdigest("john@example.com"))
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 rotation is fairly straightforward: if you add a key to the keyring with a higher id than any other key, that key will automatically be used for encryption when records are either created or updated. Any keys that are no longer in use can be safely removed from the keyring.
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 case, you can rollout a key rotation:
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 decoupled from ActiveRecord/Sequel. First, make sure you load `keyring` instead. Then you can create a keyring to encrypt/decrypt strings, without even touching the database.
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("1" => "QSXyoiRDPoJmfkJUZ4hJeQ==")
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 <https://github.com/fnando/keyring-node>, which is able to read and write messages using the same format.
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 `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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 release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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 https://github.com/fnando/attr_keyring. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
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 [MIT License](https://opensource.org/licenses/MIT).
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 [Flaticon](https://www.flaticon.com/) is licensed by Creative Commons BY 3.0.
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, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/fnando/attr_keyring/blob/master/CODE_OF_CONDUCT.md).
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
@@ -16,4 +16,4 @@ task(:rubocop) do
16
16
  RuboCop::CLI.new.run(["--config", File.join(__dir__, ".rubocop.yml")])
17
17
  end
18
18
 
19
- task :default => [:test, :rubocop] # rubocop:disable Style/HashSyntax, Style/SymbolArray
19
+ task default: %i[test rubocop]
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
- # Specify which files should be added to the gem when it is released.
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"]
@@ -2,4 +2,4 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
  gemspec path: ".."
5
- gem "activerecord", "~> 5.2.0"
5
+ gem "activerecord", "~> 6.1.0"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+ gemspec path: ".."
5
+ gem "activerecord", "~> 7.0.0.rc1"
@@ -21,7 +21,7 @@ module AttrKeyring
21
21
  def reload(options = nil)
22
22
  instance = super
23
23
 
24
- self.class.encrypted_attributes.each do |attribute|
24
+ self.class.encrypted_attributes.each do |attribute, _options|
25
25
  clear_decrypted_column_cache(attribute)
26
26
  end
27
27
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AttrKeyring
4
+ module Encoders
5
+ module JSONEncoder
6
+ def self.dump(data)
7
+ ::JSON.dump(data)
8
+ end
9
+
10
+ def self.parse(data)
11
+ ::JSON.parse(data, symbolize_names: true)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AttrKeyring
4
- VERSION = "0.5.4"
4
+ VERSION = "0.7.0"
5
5
  end
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, encryptor: Keyring::Encryptor::AES::AES128CBC)
44
- self.keyring = Keyring.new(keyring, encryptor)
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, "Expected HMAC to be #{Base64.strict_encode64(expected_hmac)}; got #{Base64.strict_encode64(hmac)} instead" # rubocop:disable Metrics/LineLength
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 Metrics/LineLength
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, encryptor)
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, encryptor = Encryptor::AES::AES128CBC)
64
- Base.new(keyring, encryptor)
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.5.4
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: 2019-11-27 00:00:00.000000000 Z
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: '0'
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.0.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=