blind_index 0.3.3 → 0.3.4

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: 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