blind_index 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26d5a803099fa8b408065a43056697f24eaf10a89c6a778979e5bdd8d54bbb5e
4
- data.tar.gz: '0288069afcdd54f3f3917ef25177550bd3c912333aca346d1ae77d0d237eb5ce'
3
+ metadata.gz: 0a5e0cf17996a671847cd6b7d9c89820c4dfa319c1f6e8c60bff430d5fdbb222
4
+ data.tar.gz: 82d7b94d12cd4391d48d845613a75740c0edbcb38d9d8272054b3bea58574c81
5
5
  SHA512:
6
- metadata.gz: 1f062a2b9d1aa2d251dd38b6c815423afa146a34df0825d655bd9ac0ea666af9d0382d958995c7899e16a729b15e7e3d696d661485a246220e93bade67104641
7
- data.tar.gz: 1478bb3716c6ad099e42ed95b286d7aa5a13c6be7f75d4ec6e7d5c82898c95d1fc7db2537d53c6daa5754ad398579bca5e924a60551423402e064f24b52ee2ff
6
+ metadata.gz: ff9d3d7b14c867a1972faa2600bfec2918fee21b2c2108bfa5a358990d1fc661c2aab84cc39c1610a0064cfd6a9b0ad461c1269b4bfdc8b85414f944235d864b
7
+ data.tar.gz: 87f28352c529b0cb61a0b4cf050a3619234da3e21fb06b5ba36d3d605119910678947994d381bb3a1bad81716300fe184d0adf8aa73cb78a95bd93ce93a23206
@@ -1,3 +1,9 @@
1
+ ## 0.3.4
2
+
3
+ - Added `size` option
4
+ - Added sanity checks for Argon2 cost parameters
5
+ - Fixed ActiveRecord callback issues introduced in 0.3.3
6
+
1
7
  ## 0.3.3
2
8
 
3
9
  - Added support for string keys in finders
data/README.md CHANGED
@@ -4,31 +4,29 @@ Securely search encrypted database fields
4
4
 
5
5
  Designed for use with [attr_encrypted](https://github.com/attr-encrypted/attr_encrypted)
6
6
 
7
- Here’s a [full example](https://ankane.org/securing-user-emails-in-rails) of how to use it with Devise
7
+ Here’s a [full example](https://ankane.org/securing-user-emails-in-rails) of how to use it
8
8
 
9
9
  [![Build Status](https://travis-ci.org/ankane/blind_index.svg?branch=master)](https://travis-ci.org/ankane/blind_index)
10
10
 
11
11
  ## How It Works
12
12
 
13
- We use [this approach](https://www.sitepoint.com/how-to-search-on-securely-encrypted-database-fields/) by Scott Arciszewski. To summarize, we compute a keyed hash of the sensitive data and store it in a column. To query, we apply the keyed hash function (PBKDF2-HMAC-SHA256 by default) to the value we’re searching and then perform a database search. This results in performant queries for equality operations, while keeping the data secure from those without the key.
13
+ We use [this approach](https://paragonie.com/blog/2017/05/building-searchable-encrypted-databases-with-php-and-sql) by Scott Arciszewski. To summarize, we compute a keyed hash of the sensitive data and store it in a column. To query, we apply the keyed hash function (PBKDF2-SHA256 by default) to the value we’re searching and then perform a database search. This results in performant queries for equality operations, while keeping the data secure from those without the key.
14
14
 
15
- ## Getting Started
15
+ ## Installation
16
16
 
17
- Add these lines to your application’s Gemfile:
17
+ Add this line to your application’s Gemfile:
18
18
 
19
19
  ```ruby
20
- gem 'attr_encrypted'
21
20
  gem 'blind_index'
22
21
  ```
23
22
 
24
- Add columns for the encrypted data and the blind index
23
+ ## Getting Started
25
24
 
26
- ```ruby
27
- # encrypted data
28
- add_column :users, :encrypted_email, :string
29
- add_column :users, :encrypted_email_iv, :string
25
+ > Note: Your model should already be set up with attr_encrypted. The examples are for a `User` model with `attr_encrypted :email`. See the [full example](https://ankane.org/securing-user-emails-in-rails) if needed.
30
26
 
31
- # blind index
27
+ Create a migration to add a column for the blind index
28
+
29
+ ```ruby
32
30
  add_column :users, :encrypted_email_bidx, :string
33
31
  add_index :users, :encrypted_email_bidx
34
32
  ```
@@ -37,24 +35,31 @@ And add to your model
37
35
 
38
36
  ```ruby
39
37
  class User < ApplicationRecord
40
- attr_encrypted :email, key: [ENV["EMAIL_ENCRYPTION_KEY"]].pack("H*")
41
38
  blind_index :email, key: [ENV["EMAIL_BLIND_INDEX_KEY"]].pack("H*")
42
39
  end
43
40
  ```
44
41
 
45
- We use environment variables to store the keys as hex-encoded strings ([dotenv](https://github.com/bkeepers/dotenv) is great for this). [Here’s an explanation](https://ankane.org/encryption-keys) of why `pack` is used. *Do not commit them to source control.* Generate one key for encryption and one key for hashing. You can generate keys in the Rails console with:
42
+ We use an environment variable to store the key as a hex-encoded string ([dotenv](https://github.com/bkeepers/dotenv) is great for this). [Here’s an explanation](https://ankane.org/encryption-keys) of why `pack` is used. *Do not commit it to source control.* This should be different than the key you use for encryption. You can generate a key in the Rails console with:
46
43
 
47
44
  ```ruby
48
45
  SecureRandom.hex(32)
49
46
  ```
50
47
 
51
- For development, you can use these:
48
+ For development, you can use this:
52
49
 
53
50
  ```sh
54
- EMAIL_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
55
51
  EMAIL_BLIND_INDEX_KEY=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
56
52
  ```
57
53
 
54
+ Backfill existing records
55
+
56
+ ```ruby
57
+ User.find_each do |user|
58
+ user.compute_email_bidx
59
+ user.save!
60
+ end
61
+ ```
62
+
58
63
  And query away
59
64
 
60
65
  ```ruby
@@ -92,7 +97,7 @@ add_column :users, :encrypted_email_ci_bidx, :string
92
97
  add_index :users, :encrypted_email_ci_bidx
93
98
  ```
94
99
 
95
- And update your model
100
+ Update your model
96
101
 
97
102
  ```ruby
98
103
  class User < ApplicationRecord
@@ -101,7 +106,16 @@ class User < ApplicationRecord
101
106
  end
102
107
  ```
103
108
 
104
- Search with:
109
+ Backfill existing records
110
+
111
+ ```ruby
112
+ User.find_each do |user|
113
+ user.compute_email_ci_bidx
114
+ user.save!
115
+ end
116
+ ```
117
+
118
+ And query away
105
119
 
106
120
  ```ruby
107
121
  User.where(email_ci: "test@example.org")
@@ -128,8 +142,9 @@ You can also use virtual attributes to index data from multiple columns:
128
142
  class User < ApplicationRecord
129
143
  attribute :initials
130
144
 
131
- # must come before blind_index method
145
+ # must come before the blind_index method so it runs first
132
146
  before_validation :set_initials, if: -> { changes.key?(:first_name) || changes.key?(:last_name) }
147
+
133
148
  blind_index :initials, ...
134
149
 
135
150
  def set_initials
@@ -140,68 +155,49 @@ end
140
155
 
141
156
  *Requires ActiveRecord 5.1+*
142
157
 
143
- ## Fixtures
144
-
145
- You can use encrypted attributes and blind indexes in fixtures with:
146
-
147
- ```yml
148
- test_user:
149
- encrypted_email: <%= User.encrypt_email("test@example.org", iv: Base64.decode64("0000000000000000")) %>
150
- encrypted_email_iv: "0000000000000000"
151
- encrypted_email_bidx: <%= User.compute_email_bidx("test@example.org").inspect %>
152
- ```
153
-
154
- Be sure to include the `inspect` at the end, or it won’t be encoded properly in YAML.
155
-
156
158
  ## Algorithms
157
159
 
158
- ### PBKDF2-HMAC-SHA256
160
+ ### PBKDF2-SHA256
159
161
 
160
- The default hashing algorithm. [Key stretching](https://en.wikipedia.org/wiki/Key_stretching) increases the amount of time required to compute hashes, which slows down brute-force attacks. You can set the number of iterations with:
162
+ The default hashing algorithm. [Key stretching](https://en.wikipedia.org/wiki/Key_stretching) increases the amount of time required to compute hashes, which slows down brute-force attacks.
163
+
164
+ The default number of iterations is 10,000. For highly sensitive fields, set this to at least 100,000.
161
165
 
162
166
  ```ruby
163
167
  class User < ApplicationRecord
164
- blind_index :email, iterations: 1000000, ...
168
+ blind_index :email, iterations: 100000, ...
165
169
  end
166
170
  ```
167
171
 
168
- The default is `10000`. Changing this value requires you to recompute the blind index.
172
+ > Changing this requires you to recompute the blind index.
173
+
174
+ ### Argon2
169
175
 
170
- ### scrypt
176
+ Argon2 is the state-of-the-art algorithm and recommended for best security.
171
177
 
172
- Add [scrypt](https://github.com/pbhogan/scrypt) to your Gemfile and use:
178
+ To use it, add [argon2](https://github.com/technion/ruby-argon2) to your Gemfile and set:
173
179
 
174
180
  ```ruby
175
181
  class User < ApplicationRecord
176
- blind_index :email, algorithm: :scrypt, ...
182
+ blind_index :email, algorithm: :argon2, ...
177
183
  end
178
184
  ```
179
185
 
180
- Set the cost parameters with:
186
+ The default cost parameters are `{t: 3, m: 12}`. For highly sensitive fields, set this to at least `{t: 4, m: 15}`.
181
187
 
182
188
  ```ruby
183
189
  class User < ApplicationRecord
184
- blind_index :email, algorithm: :scrypt, cost: {n: 4096, r: 8, p: 1}, ...
190
+ blind_index :email, algorithm: :argon2, cost: {t: 4, m: 15}, ...
185
191
  end
186
192
  ```
187
193
 
188
- ### Argon2
194
+ > Changing this requires you to recompute the blind index.
189
195
 
190
- Add [argon2](https://github.com/technion/ruby-argon2) to your Gemfile and use:
196
+ The variant used is Argon2i.
191
197
 
192
- ```ruby
193
- class User < ApplicationRecord
194
- blind_index :email, algorithm: :argon2, ...
195
- end
196
- ```
197
-
198
- Set the cost parameters with:
198
+ ### Other
199
199
 
200
- ```ruby
201
- class User < ApplicationRecord
202
- blind_index :email, algorithm: :argon2, cost: {t: 3, m: 12}, ...
203
- end
204
- ```
200
+ scrypt is [also supported](docs/scrypt.md). Unless you have specific reasons to use it, go with Argon2 instead.
205
201
 
206
202
  ## Key Rotation
207
203
 
@@ -243,6 +239,19 @@ end
243
239
 
244
240
  Finally, drop the old column.
245
241
 
242
+ ## Fixtures
243
+
244
+ You can use encrypted attributes and blind indexes in fixtures with:
245
+
246
+ ```yml
247
+ test_user:
248
+ encrypted_email: <%= User.encrypt_email("test@example.org", iv: Base64.decode64("0000000000000000")) %>
249
+ encrypted_email_iv: "0000000000000000"
250
+ encrypted_email_bidx: <%= User.compute_email_bidx("test@example.org").inspect %>
251
+ ```
252
+
253
+ Be sure to include the `inspect` at the end, or it won’t be encoded properly in YAML.
254
+
246
255
  ## Reference
247
256
 
248
257
  By default, blind indexes are encoded in Base64. Set a different encoding with:
@@ -253,6 +262,14 @@ class User < ApplicationRecord
253
262
  end
254
263
  ```
255
264
 
265
+ By default, blind indexes are 32 bytes. Set a smaller size with:
266
+
267
+ ```ruby
268
+ class User < ApplicationRecord
269
+ blind_index :email, size: 16
270
+ end
271
+ ```
272
+
256
273
  ## Alternatives
257
274
 
258
275
  One alternative to blind indexing is to use a deterministic encryption scheme, like [AES-SIV](https://github.com/miscreant/miscreant). In this approach, the encrypted data will be the same for matches.
@@ -13,7 +13,7 @@ module BlindIndex
13
13
  end
14
14
  self.default_options = {
15
15
  iterations: 10000,
16
- algorithm: :pbkdf2_hmac,
16
+ algorithm: :pbkdf2_sha256,
17
17
  insecure_key: false,
18
18
  encode: true,
19
19
  cost: {}
@@ -27,14 +27,15 @@ module BlindIndex
27
27
 
28
28
  unless value.nil?
29
29
  algorithm = options[:algorithm].to_sym
30
+ algorithm = :pbkdf2_sha256 if algorithm == :pbkdf2_hmac
31
+ algorithm = :argon2i if algorithm == :argon2
30
32
 
31
33
  key = key.call if key.respond_to?(:call)
32
34
  raise BlindIndex::Error, "Missing key for blind index" unless key
33
35
 
34
36
  key = key.to_s
35
- unless options[:insecure_key] && algorithm == :pbkdf2_hmac
37
+ unless options[:insecure_key] && algorithm == :pbkdf2_sha256
36
38
  raise BlindIndex::Error, "Key must use binary encoding" if key.encoding != Encoding::BINARY
37
- # raise BlindIndex::Error, "Key must not be ASCII" if key.bytes.all? { |b| b < 128 }
38
39
  raise BlindIndex::Error, "Key must be 32 bytes" if key.bytesize != 32
39
40
  end
40
41
 
@@ -42,20 +43,37 @@ module BlindIndex
42
43
  # https://gist.github.com/ankane/fe3ac63fbf1c4550ee12554c664d2b8c
43
44
  cost_options = options[:cost]
44
45
 
46
+ # check size
47
+ size = (options[:size] || 32).to_i
48
+ raise BlindIndex::Error, "Size must be between 1 and 32" unless (1..32).include?(size)
49
+
50
+ value = value.to_s
51
+
45
52
  value =
46
53
  case algorithm
47
54
  when :scrypt
48
55
  n = cost_options[:n] || 4096
49
56
  r = cost_options[:r] || 8
50
57
  cp = cost_options[:p] || 1
51
- SCrypt::Engine.scrypt(value.to_s, key, n, r, cp, 32)
52
- when :argon2
53
- t = cost_options[:t] || 3
54
- m = cost_options[:m] || 12
55
- [Argon2::Engine.hash_argon2i(value.to_s, key, t, m)].pack("H*")
56
- when :pbkdf2_hmac
58
+ SCrypt::Engine.scrypt(value, key, n, r, cp, size)
59
+ when :argon2i
60
+ t = (cost_options[:t] || 3).to_i
61
+ # use same bounds as rbnacl
62
+ raise BlindIndex::Error, "t must be between 3 and 10" if t < 3 || t > 10
63
+
64
+ # m is memory in kibibytes (1024 bytes)
65
+ m = (cost_options[:m] || 12).to_i
66
+ # use same bounds as rbnacl
67
+ raise BlindIndex::Error, "m must be between 3 and 22" if m < 3 || m > 22
68
+
69
+ # 32 byte digest size is limitation of argon2 gem
70
+ # this is no longer the case on master
71
+ # TODO add conditional check when next version of argon2 is released
72
+ raise BlindIndex::Error, "Size must be 32" unless size == 32
73
+ [Argon2::Engine.hash_argon2i(value, key, t, m)].pack("H*")
74
+ when :pbkdf2_sha256
57
75
  iterations = cost_options[:iterations] || options[:iterations]
58
- OpenSSL::PKCS5.pbkdf2_hmac(value.to_s, key, iterations, 32, "sha256")
76
+ OpenSSL::PKCS5.pbkdf2_hmac(value, key, iterations, size, "sha256")
59
77
  else
60
78
  raise BlindIndex::Error, "Unknown algorithm"
61
79
  end
@@ -1,6 +1,6 @@
1
1
  module BlindIndex
2
2
  module Model
3
- def blind_index(name, key: nil, iterations: nil, attribute: nil, expression: nil, bidx_attribute: nil, callback: true, algorithm: nil, insecure_key: nil, encode: nil, cost: nil)
3
+ def blind_index(name, key: nil, iterations: nil, attribute: nil, expression: nil, bidx_attribute: nil, callback: true, algorithm: nil, insecure_key: nil, encode: nil, cost: nil, size: nil)
4
4
  iterations ||= 10000
5
5
  attribute ||= name
6
6
  bidx_attribute ||= :"encrypted_#{name}_bidx"
@@ -36,7 +36,8 @@ module BlindIndex
36
36
  algorithm: algorithm,
37
37
  insecure_key: insecure_key,
38
38
  encode: encode,
39
- cost: cost
39
+ cost: cost,
40
+ size: size
40
41
  }.reject { |_, v| v.nil? }
41
42
 
42
43
  # should have been named generate_#{name}_bidx
@@ -49,12 +50,7 @@ module BlindIndex
49
50
  end
50
51
 
51
52
  if callback
52
- if ActiveRecord::VERSION::STRING >= "5.1"
53
- before_validation method_name, if: :"will_save_change_to_#{attribute}?"
54
- else
55
- before_validation method_name, if: -> { changes.key?(attribute.to_s) }
56
- end
57
-
53
+ before_validation method_name, if: -> { changes.key?(attribute.to_s) }
58
54
  end
59
55
 
60
56
  # use include so user can override
@@ -1,3 +1,3 @@
1
1
  module BlindIndex
2
- VERSION = "0.3.3"
2
+ VERSION = "0.3.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blind_index
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-12 00:00:00.000000000 Z
11
+ date: 2018-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -183,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
183
  version: '0'
184
184
  requirements: []
185
185
  rubyforge_project:
186
- rubygems_version: 2.7.7
186
+ rubygems_version: 2.7.6
187
187
  signing_key:
188
188
  specification_version: 4
189
189
  summary: Securely search encrypted database fields