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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf52bc4c9faa76f061e0023bf8bf21cfe9de59a5ff1ce608392b388957be85db
4
- data.tar.gz: 3b5e4145a2704b00b5784b49fd949172e66629499d0eccaaf5f237b8a1828b70
3
+ metadata.gz: d02280da9cb28259980ea283b6030672957657ead8b895594176eef09d78382b
4
+ data.tar.gz: 88ddda0bb1d9a85869246e46ab49a8f4889bc0c398d616be9dd162eec239a177
5
5
  SHA512:
6
- metadata.gz: bf408bfc13aa86d353d345c47a08a40d04113408f3a7d20c9a7ba0e6cc347c12b5adf533fe0c250764cc0df330b283a96ee2a6db4cacff7ce61b20c07614772d
7
- data.tar.gz: 4a38d54dbf79ad34a9b9a54bbdbc655382e4ab128209ec4f73fbcb62df46b0040519a47c394dffabc4b567e5c852a89c91ee6bfe5d35c8ed4b10dfbe1943b270
6
+ metadata.gz: 6a3108027fac7dbfee097e7afa3227f58f33560e540f19bc0f0b63cb4e482e8315ffafdbd4360bc31724426c9150109c6b9e46e9ecdf70526ca13cb34a8bee93
7
+ data.tar.gz: 16a5ca41d03b434dbf2eb4a5751af6595e7d00c93867159c3990a3e1d0a104d170dfd434580eff0420b1b966afe0877ec70362c6d66cad34e89e0f5fed8867d4
@@ -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,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
- Include:
4
- - '**/*.gemspec'
5
- - '**/*.podspec'
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
- Layout/FirstArrayElementLineBreak:
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
- Style/EmptyMethod:
61
- EnforcedStyle: expanded
14
+ Layout/LineLength:
15
+ Exclude:
16
+ - test/**/*
62
17
 
63
- Style/FrozenStringLiteralComment:
18
+ Metrics/MethodLength:
64
19
  Enabled: false
65
20
 
66
- Style/AccessModifierDeclarations:
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
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
 
@@ -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 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.
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 keys by using the following command:
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 in the keyring. Half this key is used for encryption, and half for the HMAC.
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 the size as half of it is used for HMAC computation.
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 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.
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 `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.
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 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:
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 larger id is assumed to be newer. The value is the actual bytes of the encryption key.
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 `credentials.yml` must be define like the following:
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 `attr_keyring Rails.application.credentials.user_keyring`.
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 management):
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 above.
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 be saved. This will allow you to lookup by that value instead and add unique indexes.
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: Digest::SHA1.hexdigest("john@example.com"))
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 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.
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 case, you can rollout a key rotation:
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 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.
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("1" => "QSXyoiRDPoJmfkJUZ4hJeQ==")
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 <https://github.com/fnando/keyring-node>, which is able to read and write messages using the same format.
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 `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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 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).
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 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.
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 [MIT License](https://opensource.org/licenses/MIT).
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 [Flaticon](https://www.flaticon.com/) is licensed by Creative Commons BY 3.0.
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, 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).
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 :default => [:test, :rubocop] # rubocop:disable Style/HashSyntax, Style/SymbolArray
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
- # Specify which files should be added to the gem when it is released.
15
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
16
- spec.files = Dir.chdir(File.expand_path(__dir__)) do
17
- `git ls-files -z`.split("\x0").reject {|f| f.match(%r{^(test|spec|features)/}) }
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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "bundler/setup"
4
5
  require "attr_keyring"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/inline"
2
4
  require "stringio"
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/inline"
2
4
 
3
5
  gemfile do
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/inline"
2
4
  require "stringio"
3
5
 
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+ gemspec path: ".."
5
+ gem "activerecord", "~> 6.0.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"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_record"
2
4
 
3
5
  module AttrKeyring
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sequel"
2
4
 
3
5
  module AttrKeyring
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrKeyring
2
- VERSION = "0.5.2".freeze
4
+ VERSION = "0.6.1"
3
5
  end
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, encryptor: Keyring::Encryptor::AES::AES128CBC)
42
- self.keyring = Keyring.new(keyring, encryptor)
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 = self.class.keyring.encrypt(value, previous_keyring_id)
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
- public_send("#{attribute}_digest=", digest) if respond_to?("#{attribute}_digest=")
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
- return instance_variable_get(cache_name) if instance_variable_defined?(cache_name)
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(encrypted_value, public_send(self.class.keyring_column_name))
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
- remove_instance_variable(cache_name) if instance_variable_defined?(cache_name)
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
- public_send("#{attribute}_digest=", nil) if respond_to?("#{attribute}_digest=")
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
- public_send("#{attribute}_digest=", digest) if respond_to?("#{attribute}_digest")
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
- raise InvalidAuthentication, "Expected HMAC to be #{Base64.strict_encode64(expected_hmac)}; got #{Base64.strict_encode64(hmac)} instead" unless verify_signature(expected_hmac, hmac)
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
- alias_method :inspect, :to_s
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
- raise InvalidSecret, "Secret must be #{expected_key_size} bytes, instead got #{secret.bytesize}" unless secret.bytesize == expected_key_size
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, encryptor)
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, encryptor = Encryptor::AES::AES128CBC)
62
- 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)
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.5.2
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: 2019-10-20 00:00:00.000000000 Z
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: '0'
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.0.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=