blind_index 0.3.5 → 1.0.0
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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +149 -87
- data/lib/blind_index.rb +56 -29
- data/lib/blind_index/key_generator.rb +54 -0
- data/lib/blind_index/model.rb +75 -50
- data/lib/blind_index/version.rb +1 -1
- metadata +29 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2a4c6e35691fe1efa918f077afda9902109591f695a3f4043ae9eb6350ffb44
|
4
|
+
data.tar.gz: dcbe4e3c535a2d36495370a7fa53078d6fd32979c8b7d07b7e29c3202e8fd393
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c80bec06e019677e606d4da2c52d0aa529f5aeb56e1091a79a138ce40b73dedb3244fdabf073a96d249e9a876e6557d73e38bd48dfddfd5b1963e19ebb91c93
|
7
|
+
data.tar.gz: e206e2619af60a2274f300eac56f1edf3a7f00822c8bdead7fa579252b81b7540d16832c083ed630f454c08049521b312c95f72c33e070706894a60f1c3f1eef
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
## 1.0.0
|
2
|
+
|
3
|
+
- Added support for master key
|
4
|
+
- Added support for Argon2id
|
5
|
+
- Fixed `generate_key` for JRuby
|
6
|
+
- Dropped support for Rails 4.2
|
7
|
+
|
8
|
+
Breaking changes
|
9
|
+
|
10
|
+
- Made Argon2id the default algorithm
|
11
|
+
- Removed `encrypted_` prefix from columns
|
12
|
+
- Changed default encoding to Base64 strict
|
13
|
+
|
1
14
|
## 0.3.5
|
2
15
|
|
3
16
|
- Added support for hex keys
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Securely search encrypted database fields
|
4
4
|
|
5
|
-
|
5
|
+
Works with [Lockbox](https://github.com/ankane/lockbox) and [attr_encrypted](https://github.com/attr-encrypted/attr_encrypted)
|
6
6
|
|
7
7
|
Here’s a [full example](https://ankane.org/securing-user-emails-in-rails) of how to use it
|
8
8
|
|
@@ -28,45 +28,67 @@ Add this line to your application’s Gemfile:
|
|
28
28
|
gem 'blind_index'
|
29
29
|
```
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
> 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.
|
34
|
-
|
35
|
-
Create a migration to add a column for the blind index
|
31
|
+
On Windows, also add:
|
36
32
|
|
37
33
|
```ruby
|
38
|
-
|
39
|
-
add_index :users, :encrypted_email_bidx
|
34
|
+
gem 'argon2', git: 'https://github.com/technion/ruby-argon2.git', submodules: true
|
40
35
|
```
|
41
36
|
|
42
|
-
|
37
|
+
Until `argon2 >= 2.0.1` is released.
|
38
|
+
|
39
|
+
## Getting Started
|
40
|
+
|
41
|
+
> Note: Your model should already be set up with Lockbox or attr_encrypted. The examples are for a `User` model with `encrypts :email` or `attr_encrypted :email`. See the [full example](https://ankane.org/securing-user-emails-in-rails) if needed.
|
42
|
+
|
43
|
+
First, generate a key
|
43
44
|
|
44
45
|
```ruby
|
45
46
|
BlindIndex.generate_key
|
46
47
|
```
|
47
48
|
|
48
|
-
Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this). Be sure to use different keys in development and production
|
49
|
+
Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this). Be sure to use different keys in development and production. Keys don’t need to be hex-encoded, but it’s often easier to store them this way.
|
49
50
|
|
50
|
-
|
51
|
+
Set the following environment variable with your key (you can use this one in development)
|
51
52
|
|
52
53
|
```sh
|
53
|
-
|
54
|
+
BLIND_INDEX_MASTER_KEY=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
|
55
|
+
```
|
56
|
+
|
57
|
+
or create `config/initializers/blind_index.rb` with something like
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
BlindIndex.master_key = Rails.application.credentials.blind_index_master_key
|
61
|
+
```
|
62
|
+
|
63
|
+
Create a migration to add a column for the blind index
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
add_column :users, :email_bidx, :string
|
67
|
+
add_index :users, :email_bidx # unique: true if needed
|
54
68
|
```
|
55
69
|
|
56
70
|
Add to your model
|
57
71
|
|
58
72
|
```ruby
|
59
73
|
class User < ApplicationRecord
|
60
|
-
blind_index :email
|
74
|
+
blind_index :email
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
For more sensitive fields, use
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class User < ApplicationRecord
|
82
|
+
blind_index :email, slow: true
|
61
83
|
end
|
62
84
|
```
|
63
85
|
|
64
86
|
Backfill existing records
|
65
87
|
|
66
88
|
```ruby
|
67
|
-
User.find_each do |user|
|
89
|
+
User.unscoped.where(email_bidx: nil).find_each do |user|
|
68
90
|
user.compute_email_bidx
|
69
|
-
user.save
|
91
|
+
user.save(validate: false)
|
70
92
|
end
|
71
93
|
```
|
72
94
|
|
@@ -94,7 +116,7 @@ You can apply expressions to attributes before indexing and searching. This give
|
|
94
116
|
|
95
117
|
```ruby
|
96
118
|
class User < ApplicationRecord
|
97
|
-
blind_index :email, expression: ->(v) { v.downcase }
|
119
|
+
blind_index :email, expression: ->(v) { v.downcase }
|
98
120
|
end
|
99
121
|
```
|
100
122
|
|
@@ -103,25 +125,25 @@ end
|
|
103
125
|
You may want multiple blind indexes for an attribute. To do this, add another column:
|
104
126
|
|
105
127
|
```ruby
|
106
|
-
add_column :users, :
|
107
|
-
add_index :users, :
|
128
|
+
add_column :users, :email_ci_bidx, :string
|
129
|
+
add_index :users, :email_ci_bidx
|
108
130
|
```
|
109
131
|
|
110
132
|
Update your model
|
111
133
|
|
112
134
|
```ruby
|
113
135
|
class User < ApplicationRecord
|
114
|
-
blind_index :email
|
115
|
-
blind_index :email_ci, attribute: :email, expression: ->(v) { v.downcase }
|
136
|
+
blind_index :email
|
137
|
+
blind_index :email_ci, attribute: :email, expression: ->(v) { v.downcase }
|
116
138
|
end
|
117
139
|
```
|
118
140
|
|
119
141
|
Backfill existing records
|
120
142
|
|
121
143
|
```ruby
|
122
|
-
User.find_each do |user|
|
144
|
+
User.unscoped.where(email_ci_bidx: nil).find_each do |user|
|
123
145
|
user.compute_email_ci_bidx
|
124
|
-
user.save
|
146
|
+
user.save(validate: false)
|
125
147
|
end
|
126
148
|
```
|
127
149
|
|
@@ -137,25 +159,23 @@ If you don’t need to store the original value (for instance, when just checkin
|
|
137
159
|
|
138
160
|
```ruby
|
139
161
|
class User < ApplicationRecord
|
140
|
-
attribute :email
|
141
|
-
blind_index :email
|
162
|
+
attribute :email, :string
|
163
|
+
blind_index :email
|
142
164
|
end
|
143
165
|
```
|
144
166
|
|
145
|
-
*Requires ActiveRecord 5.1+*
|
146
|
-
|
147
167
|
## Multiple Columns
|
148
168
|
|
149
169
|
You can also use virtual attributes to index data from multiple columns:
|
150
170
|
|
151
171
|
```ruby
|
152
172
|
class User < ApplicationRecord
|
153
|
-
attribute :initials
|
173
|
+
attribute :initials, :string
|
154
174
|
|
155
175
|
# must come before the blind_index method so it runs first
|
156
176
|
before_validation :set_initials, if: -> { changes.key?(:first_name) || changes.key?(:last_name) }
|
157
177
|
|
158
|
-
blind_index :initials
|
178
|
+
blind_index :initials
|
159
179
|
|
160
180
|
def set_initials
|
161
181
|
self.initials = "#{first_name[0]}#{last_name[0]}"
|
@@ -163,134 +183,176 @@ class User < ApplicationRecord
|
|
163
183
|
end
|
164
184
|
```
|
165
185
|
|
166
|
-
|
167
|
-
|
168
|
-
## Algorithms
|
186
|
+
## Key Rotation
|
169
187
|
|
170
|
-
|
188
|
+
To rotate keys without downtime, add a new column:
|
171
189
|
|
172
|
-
|
190
|
+
```ruby
|
191
|
+
add_column :users, :email_bidx_v2, :string
|
192
|
+
add_index :users, :email_bidx_v2
|
193
|
+
```
|
173
194
|
|
174
|
-
|
195
|
+
And add to your model
|
175
196
|
|
176
197
|
```ruby
|
177
198
|
class User < ApplicationRecord
|
178
|
-
blind_index :email,
|
199
|
+
blind_index :email, rotate: {version: 2, master_key: ENV["BLIND_INDEX_MASTER_KEY_V2"]}
|
179
200
|
end
|
180
201
|
```
|
181
202
|
|
182
|
-
|
183
|
-
|
184
|
-
### Argon2
|
203
|
+
This will keep the new column synced going forward. Next, backfill the data:
|
185
204
|
|
186
|
-
|
205
|
+
```ruby
|
206
|
+
User.unscoped.where(email_bidx_v2: nil).find_each do |user|
|
207
|
+
user.compute_rotated_email_bidx
|
208
|
+
user.save(validate: false)
|
209
|
+
end
|
210
|
+
```
|
187
211
|
|
188
|
-
|
212
|
+
Then update your model
|
189
213
|
|
190
214
|
```ruby
|
191
215
|
class User < ApplicationRecord
|
192
|
-
blind_index :email,
|
216
|
+
blind_index :email, version: 2, master_key: ENV["BLIND_INDEX_MASTER_KEY_V2"]
|
193
217
|
end
|
194
218
|
```
|
195
219
|
|
196
|
-
|
220
|
+
Finally, drop the old column.
|
221
|
+
|
222
|
+
## Key Separation
|
223
|
+
|
224
|
+
The master key is used to generate unique keys for each blind index. This technique comes from [CipherSweet](https://ciphersweet.paragonie.com/internals/key-hierarchy). The table name and blind index column name are both used in this process. If you need to rename a table with blind indexes, or a blind index column itself, get the key:
|
225
|
+
|
226
|
+
```ruby
|
227
|
+
BlindIndex.index_key(table: "users", bidx_attribute: "email_bidx")
|
228
|
+
```
|
229
|
+
|
230
|
+
And set it directly before renaming:
|
197
231
|
|
198
232
|
```ruby
|
199
233
|
class User < ApplicationRecord
|
200
|
-
blind_index :email,
|
234
|
+
blind_index :email, key: ENV["USER_EMAIL_BLIND_INDEX_KEY"]
|
201
235
|
end
|
202
236
|
```
|
203
237
|
|
204
|
-
|
238
|
+
## Algorithm
|
205
239
|
|
206
|
-
|
240
|
+
Argon2id is used for best security. The default cost parameters are 3 iterations and 4 MB of memory. For `slow: true`, the cost parameters are 4 iterations and 32 MB of memory.
|
207
241
|
|
208
|
-
|
242
|
+
A number of other algorithms are [also supported](docs/Other-Algorithms.md). Unless you have specific reasons to use them, go with Argon2id.
|
209
243
|
|
210
|
-
|
244
|
+
## Fixtures
|
211
245
|
|
212
|
-
|
246
|
+
You can use blind indexes in fixtures with:
|
213
247
|
|
214
|
-
|
248
|
+
```yml
|
249
|
+
test_user:
|
250
|
+
email_bidx: <%= User.generate_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
|
+
|
255
|
+
## Reference
|
256
|
+
|
257
|
+
Set default options in an initializer with:
|
215
258
|
|
216
259
|
```ruby
|
217
|
-
|
218
|
-
add_index :users, :encrypted_email_v2_bidx
|
260
|
+
BlindIndex.default_options = {algorithm: :pbkdf2_sha256}
|
219
261
|
```
|
220
262
|
|
221
|
-
|
263
|
+
By default, blind indexes are encoded in Base64. Set a different encoding with:
|
222
264
|
|
223
265
|
```ruby
|
224
266
|
class User < ApplicationRecord
|
225
|
-
blind_index :email,
|
226
|
-
blind_index :email_v2, attribute: :email, key: ENV["EMAIL_V2_BLIND_INDEX_KEY"]
|
267
|
+
blind_index :email, encode: ->(v) { [v].pack("H*") }
|
227
268
|
end
|
228
269
|
```
|
229
270
|
|
230
|
-
|
271
|
+
By default, blind indexes are 32 bytes. Set a smaller size with:
|
231
272
|
|
232
273
|
```ruby
|
233
|
-
User
|
234
|
-
|
235
|
-
user.save!
|
274
|
+
class User < ApplicationRecord
|
275
|
+
blind_index :email, size: 16
|
236
276
|
end
|
237
277
|
```
|
238
278
|
|
239
|
-
|
279
|
+
Set a key directly for an index with:
|
240
280
|
|
241
281
|
```ruby
|
242
282
|
class User < ApplicationRecord
|
243
|
-
blind_index :email,
|
244
|
-
|
245
|
-
# remove this line after dropping column
|
246
|
-
self.ignored_columns = ["encrypted_email_bidx"]
|
283
|
+
blind_index :email, key: ENV["USER_EMAIL_BLIND_INDEX_KEY"]
|
247
284
|
end
|
248
285
|
```
|
249
286
|
|
250
|
-
|
287
|
+
## Alternatives
|
251
288
|
|
252
|
-
|
289
|
+
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.
|
290
|
+
|
291
|
+
## Upgrading
|
253
292
|
|
254
|
-
|
293
|
+
### 1.0.0
|
255
294
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
295
|
+
1.0.0 brings a number of improvements. Here are a few to be aware of:
|
296
|
+
|
297
|
+
- Argon2id is the default algorithm for stronger security
|
298
|
+
- You can use a master key instead of individual keys for each column
|
299
|
+
- Columns no longer have an `encrypted_` prefix
|
300
|
+
|
301
|
+
For existing fields, add:
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
class User < ApplicationRecord
|
305
|
+
blind_index :email, legacy: true
|
306
|
+
end
|
261
307
|
```
|
262
308
|
|
263
|
-
|
309
|
+
#### Optional
|
264
310
|
|
265
|
-
|
311
|
+
To rotate to new fields that use Argon2id and a master key, generate a master key:
|
266
312
|
|
267
|
-
|
313
|
+
```ruby
|
314
|
+
BlindIndex.generate_key
|
315
|
+
```
|
316
|
+
|
317
|
+
And set `ENV["BLIND_INDEX_MASTER_KEY"]` or `BlindIndex.master_key`.
|
318
|
+
|
319
|
+
Add a new column without the `encrypted_` prefix:
|
268
320
|
|
269
321
|
```ruby
|
270
|
-
|
322
|
+
add_column :users, :email_bidx, :string
|
323
|
+
add_index :users, :email_bidx # unique: true if needed
|
271
324
|
```
|
272
325
|
|
273
|
-
|
326
|
+
And add to your model
|
274
327
|
|
275
328
|
```ruby
|
276
329
|
class User < ApplicationRecord
|
277
|
-
blind_index :email,
|
330
|
+
blind_index :email, key: ENV["USER_EMAIL_BLIND_INDEX_KEY"], legacy: true, rotate: true
|
278
331
|
end
|
279
332
|
```
|
280
333
|
|
281
|
-
|
334
|
+
> For more sensitive fields, use `rotate: {slow: true}`
|
335
|
+
|
336
|
+
This will keep the new column synced going forward. Next, backfill the data:
|
282
337
|
|
283
338
|
```ruby
|
284
|
-
|
285
|
-
|
339
|
+
User.unscoped.where(email_bidx: nil).find_each do |user|
|
340
|
+
user.compute_rotated_email_bidx
|
341
|
+
user.save(validate: false)
|
286
342
|
end
|
287
343
|
```
|
288
344
|
|
289
|
-
|
345
|
+
Then update your model
|
290
346
|
|
291
|
-
|
347
|
+
```ruby
|
348
|
+
class User < ApplicationRecord
|
349
|
+
blind_index :email
|
350
|
+
end
|
351
|
+
```
|
292
352
|
|
293
|
-
|
353
|
+
> For more sensitive fields, add `slow: true`
|
354
|
+
|
355
|
+
Finally, drop the old column.
|
294
356
|
|
295
357
|
### 0.3.0
|
296
358
|
|
@@ -306,16 +368,16 @@ Update your model to convert the hex key to binary.
|
|
306
368
|
|
307
369
|
```ruby
|
308
370
|
class User < ApplicationRecord
|
309
|
-
blind_index :email, key: [ENV["
|
371
|
+
blind_index :email, key: [ENV["USER_EMAIL_BLIND_INDEX_KEY"]].pack("H*")
|
310
372
|
end
|
311
373
|
```
|
312
374
|
|
313
375
|
And recompute the blind index.
|
314
376
|
|
315
377
|
```ruby
|
316
|
-
User.find_each do |user|
|
378
|
+
User.unscoped.find_each do |user|
|
317
379
|
user.compute_email_bidx
|
318
|
-
user.save
|
380
|
+
user.save(validate: false)
|
319
381
|
end
|
320
382
|
```
|
321
383
|
|
@@ -323,7 +385,7 @@ To continue without rotating, set:
|
|
323
385
|
|
324
386
|
```ruby
|
325
387
|
class User < ApplicationRecord
|
326
|
-
blind_index :email, insecure_key: true
|
388
|
+
blind_index :email, insecure_key: true
|
327
389
|
end
|
328
390
|
```
|
329
391
|
|
data/lib/blind_index.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
# dependencies
|
2
2
|
require "active_support"
|
3
|
+
require "openssl"
|
4
|
+
require "argon2"
|
3
5
|
|
4
6
|
# modules
|
7
|
+
require "blind_index/key_generator"
|
5
8
|
require "blind_index/model"
|
6
9
|
require "blind_index/version"
|
7
10
|
|
@@ -10,23 +13,24 @@ module BlindIndex
|
|
10
13
|
|
11
14
|
class << self
|
12
15
|
attr_accessor :default_options
|
16
|
+
attr_writer :master_key
|
13
17
|
end
|
14
18
|
self.default_options = {}
|
15
19
|
|
20
|
+
def self.master_key
|
21
|
+
@master_key ||= ENV["BLIND_INDEX_MASTER_KEY"]
|
22
|
+
end
|
23
|
+
|
16
24
|
def self.generate_bidx(value, key:, **options)
|
17
25
|
options = {
|
18
|
-
|
19
|
-
algorithm: :pbkdf2_sha256,
|
20
|
-
insecure_key: false,
|
21
|
-
encode: true,
|
22
|
-
cost: {}
|
26
|
+
encode: true
|
23
27
|
}.merge(default_options).merge(options)
|
24
28
|
|
25
29
|
# apply expression
|
26
30
|
value = options[:expression].call(value) if options[:expression]
|
27
31
|
|
28
32
|
unless value.nil?
|
29
|
-
algorithm = options[:algorithm].to_sym
|
33
|
+
algorithm = (options[:algorithm] || (options[:legacy] ? :pbkdf2_sha256 : :argon2id)).to_sym
|
30
34
|
algorithm = :pbkdf2_sha256 if algorithm == :pbkdf2_hmac
|
31
35
|
algorithm = :argon2i if algorithm == :argon2
|
32
36
|
|
@@ -35,18 +39,12 @@ module BlindIndex
|
|
35
39
|
|
36
40
|
key = key.to_s
|
37
41
|
unless options[:insecure_key] && algorithm == :pbkdf2_sha256
|
38
|
-
|
39
|
-
if key.encoding != Encoding::BINARY && key =~ /\A[0-9a-f]{64}\z/i
|
40
|
-
key = [key].pack("H*")
|
41
|
-
end
|
42
|
-
|
43
|
-
raise BlindIndex::Error, "Key must use binary encoding" if key.encoding != Encoding::BINARY
|
44
|
-
raise BlindIndex::Error, "Key must be 32 bytes" if key.bytesize != 32
|
42
|
+
key = decode_key(key)
|
45
43
|
end
|
46
44
|
|
47
45
|
# gist to compare algorithm results
|
48
46
|
# https://gist.github.com/ankane/fe3ac63fbf1c4550ee12554c664d2b8c
|
49
|
-
cost_options = options[:cost]
|
47
|
+
cost_options = options[:cost] || {}
|
50
48
|
|
51
49
|
# check size
|
52
50
|
size = (options[:size] || 32).to_i
|
@@ -56,11 +54,20 @@ module BlindIndex
|
|
56
54
|
|
57
55
|
value =
|
58
56
|
case algorithm
|
59
|
-
when :
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
57
|
+
when :argon2id
|
58
|
+
t = (cost_options[:t] || (options[:slow] ? 4 : 3)).to_i
|
59
|
+
# use same bounds as rbnacl
|
60
|
+
raise BlindIndex::Error, "t must be between 3 and 10" if t < 3 || t > 10
|
61
|
+
|
62
|
+
# m is memory in kibibytes (1024 bytes)
|
63
|
+
m = (cost_options[:m] || (options[:slow] ? 15 : 12)).to_i
|
64
|
+
# use same bounds as rbnacl
|
65
|
+
raise BlindIndex::Error, "m must be between 3 and 22" if m < 3 || m > 22
|
66
|
+
|
67
|
+
[Argon2::Engine.hash_argon2id(value, key, t, m, size)].pack("H*")
|
68
|
+
when :pbkdf2_sha256
|
69
|
+
iterations = cost_options[:iterations] || options[:iterations] || (options[:slow] ? 100000 : 10000)
|
70
|
+
OpenSSL::PKCS5.pbkdf2_hmac(value, key, iterations, size, "sha256")
|
64
71
|
when :argon2i
|
65
72
|
t = (cost_options[:t] || 3).to_i
|
66
73
|
# use same bounds as rbnacl
|
@@ -71,14 +78,12 @@ module BlindIndex
|
|
71
78
|
# use same bounds as rbnacl
|
72
79
|
raise BlindIndex::Error, "m must be between 3 and 22" if m < 3 || m > 22
|
73
80
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
[
|
79
|
-
|
80
|
-
iterations = cost_options[:iterations] || options[:iterations]
|
81
|
-
OpenSSL::PKCS5.pbkdf2_hmac(value, key, iterations, size, "sha256")
|
81
|
+
[Argon2::Engine.hash_argon2i(value, key, t, m, size)].pack("H*")
|
82
|
+
when :scrypt
|
83
|
+
n = cost_options[:n] || 4096
|
84
|
+
r = cost_options[:r] || 8
|
85
|
+
cp = cost_options[:p] || 1
|
86
|
+
SCrypt::Engine.scrypt(value, key, n, r, cp, size)
|
82
87
|
else
|
83
88
|
raise BlindIndex::Error, "Unknown algorithm"
|
84
89
|
end
|
@@ -88,7 +93,7 @@ module BlindIndex
|
|
88
93
|
if encode.respond_to?(:call)
|
89
94
|
encode.call(value)
|
90
95
|
else
|
91
|
-
[value].pack("m")
|
96
|
+
[value].pack(options[:legacy] ? "m" : "m0")
|
92
97
|
end
|
93
98
|
else
|
94
99
|
value
|
@@ -98,7 +103,29 @@ module BlindIndex
|
|
98
103
|
|
99
104
|
def self.generate_key
|
100
105
|
require "securerandom"
|
101
|
-
|
106
|
+
# force encoding to make JRuby consistent with MRI
|
107
|
+
SecureRandom.hex(32).force_encoding(Encoding::US_ASCII)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.index_key(table:, bidx_attribute:, master_key: nil, encode: true)
|
111
|
+
master_key ||= BlindIndex.master_key
|
112
|
+
raise BlindIndex::Error, "Missing master key" unless master_key
|
113
|
+
|
114
|
+
key = BlindIndex::KeyGenerator.new(master_key).index_key(table: table, bidx_attribute: bidx_attribute)
|
115
|
+
key = key.unpack("H*").first if encode
|
116
|
+
key
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.decode_key(key)
|
120
|
+
# decode hex key
|
121
|
+
if key.encoding != Encoding::BINARY && key =~ /\A[0-9a-f]{64}\z/i
|
122
|
+
key = [key].pack("H*")
|
123
|
+
end
|
124
|
+
|
125
|
+
raise BlindIndex::Error, "Key must use binary encoding" if key.encoding != Encoding::BINARY
|
126
|
+
raise BlindIndex::Error, "Key must be 32 bytes" if key.bytesize != 32
|
127
|
+
|
128
|
+
key
|
102
129
|
end
|
103
130
|
end
|
104
131
|
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module BlindIndex
|
2
|
+
class KeyGenerator
|
3
|
+
def initialize(master_key)
|
4
|
+
@master_key = master_key
|
5
|
+
end
|
6
|
+
|
7
|
+
# pattern ported from CipherSweet
|
8
|
+
# https://ciphersweet.paragonie.com/internals/key-hierarchy
|
9
|
+
def index_key(table:, bidx_attribute:)
|
10
|
+
raise ArgumentError, "Missing table for key generation" if table.to_s.empty?
|
11
|
+
raise ArgumentError, "Missing field for key generation" if bidx_attribute.to_s.empty?
|
12
|
+
|
13
|
+
c = "\x7E"*32
|
14
|
+
root_key = hkdf(BlindIndex.decode_key(@master_key), salt: table.to_s, info: "#{c}#{bidx_attribute}", length: 32, hash: "sha384")
|
15
|
+
hash_hmac("sha256", pack([table, bidx_attribute, bidx_attribute]), root_key)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def hash_hmac(hash, ikm, salt)
|
21
|
+
OpenSSL::HMAC.digest(hash, salt, ikm)
|
22
|
+
end
|
23
|
+
|
24
|
+
def hkdf(ikm, salt:, info:, length:, hash:)
|
25
|
+
if OpenSSL::KDF.respond_to?(:hkdf)
|
26
|
+
return OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: length, hash: hash)
|
27
|
+
end
|
28
|
+
|
29
|
+
prk = hash_hmac(hash, ikm, salt)
|
30
|
+
|
31
|
+
# empty binary string
|
32
|
+
t = String.new
|
33
|
+
last_block = String.new
|
34
|
+
block_index = 1
|
35
|
+
while t.bytesize < length
|
36
|
+
last_block = hash_hmac(hash, last_block + info + [block_index].pack("C"), prk)
|
37
|
+
t << last_block
|
38
|
+
block_index += 1
|
39
|
+
end
|
40
|
+
|
41
|
+
t[0, length]
|
42
|
+
end
|
43
|
+
|
44
|
+
def pack(pieces)
|
45
|
+
output = String.new
|
46
|
+
output << [pieces.size].pack("V")
|
47
|
+
pieces.map(&:to_s).each do |piece|
|
48
|
+
output << [piece.bytesize].pack("Q<")
|
49
|
+
output << piece
|
50
|
+
end
|
51
|
+
output
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/blind_index/model.rb
CHANGED
@@ -1,60 +1,85 @@
|
|
1
1
|
module BlindIndex
|
2
2
|
module Model
|
3
|
-
def blind_index(
|
4
|
-
|
5
|
-
|
6
|
-
bidx_attribute ||= :"encrypted_#{name}_bidx"
|
7
|
-
|
8
|
-
name = name.to_sym
|
9
|
-
attribute = attribute.to_sym
|
10
|
-
method_name = :"compute_#{name}_bidx"
|
11
|
-
|
12
|
-
class_eval do
|
13
|
-
@blind_indexes ||= {}
|
14
|
-
|
15
|
-
unless respond_to?(:blind_indexes)
|
16
|
-
def self.blind_indexes
|
17
|
-
parent_indexes =
|
18
|
-
if superclass.respond_to?(:blind_indexes)
|
19
|
-
superclass.blind_indexes
|
20
|
-
else
|
21
|
-
{}
|
22
|
-
end
|
23
|
-
|
24
|
-
parent_indexes.merge(@blind_indexes || {})
|
25
|
-
end
|
26
|
-
end
|
3
|
+
def blind_index(*attributes, rotate: false, migrating: false, **opts)
|
4
|
+
indexes = attributes.map { |a| [a, opts.dup] }
|
5
|
+
indexes.concat(attributes.map { |a| [a, rotate.merge(rotate: true)] }) if rotate
|
27
6
|
|
28
|
-
|
29
|
-
|
30
|
-
@blind_indexes[name] = {
|
31
|
-
key: key,
|
32
|
-
iterations: iterations,
|
33
|
-
attribute: attribute,
|
34
|
-
expression: expression,
|
35
|
-
bidx_attribute: bidx_attribute,
|
36
|
-
algorithm: algorithm,
|
37
|
-
insecure_key: insecure_key,
|
38
|
-
encode: encode,
|
39
|
-
cost: cost,
|
40
|
-
size: size
|
41
|
-
}.reject { |_, v| v.nil? }
|
42
|
-
|
43
|
-
# should have been named generate_#{name}_bidx
|
44
|
-
define_singleton_method method_name do |value|
|
45
|
-
BlindIndex.generate_bidx(value, blind_indexes[name])
|
46
|
-
end
|
7
|
+
indexes.each do |name, options|
|
8
|
+
rotate = options.delete(:rotate)
|
47
9
|
|
48
|
-
|
49
|
-
|
50
|
-
|
10
|
+
# check here so we validate rotate options as well
|
11
|
+
unknown_keywords = options.keys - [:algorithm, :attribute, :bidx_attribute,
|
12
|
+
:callback, :cost, :encode, :expression, :insecure_key, :iterations, :key,
|
13
|
+
:legacy, :master_key, :size, :slow]
|
14
|
+
raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
|
51
15
|
|
52
|
-
|
53
|
-
|
16
|
+
attribute = options[:attribute] || name
|
17
|
+
version = (options[:version] || 1).to_i
|
18
|
+
callback = options[:callback].nil? ? true : options[:callback]
|
19
|
+
if options[:bidx_attribute]
|
20
|
+
bidx_attribute = options[:bidx_attribute]
|
21
|
+
else
|
22
|
+
bidx_attribute = name
|
23
|
+
bidx_attribute = "encrypted_#{bidx_attribute}" if options[:legacy]
|
24
|
+
bidx_attribute = "#{bidx_attribute}_bidx"
|
25
|
+
bidx_attribute = "#{bidx_attribute}_v#{version}" if version != 1
|
54
26
|
end
|
55
27
|
|
56
|
-
|
57
|
-
|
28
|
+
name = "migrated_#{name}" if migrating
|
29
|
+
name = "rotated_#{name}" if rotate
|
30
|
+
name = name.to_sym
|
31
|
+
attribute = attribute.to_sym
|
32
|
+
method_name = :"compute_#{name}_bidx"
|
33
|
+
class_method_name = :"generate_#{name}_bidx"
|
34
|
+
|
35
|
+
key = options[:key]
|
36
|
+
key ||= -> { BlindIndex.index_key(table: table_name, bidx_attribute: bidx_attribute, master_key: options[:master_key], encode: false) }
|
37
|
+
|
38
|
+
class_eval do
|
39
|
+
@blind_indexes ||= {}
|
40
|
+
|
41
|
+
unless respond_to?(:blind_indexes)
|
42
|
+
def self.blind_indexes
|
43
|
+
parent_indexes =
|
44
|
+
if superclass.respond_to?(:blind_indexes)
|
45
|
+
superclass.blind_indexes
|
46
|
+
else
|
47
|
+
{}
|
48
|
+
end
|
49
|
+
|
50
|
+
parent_indexes.merge(@blind_indexes || {})
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
raise BlindIndex::Error, "Duplicate blind index: #{name}" if blind_indexes[name]
|
55
|
+
|
56
|
+
@blind_indexes[name] = options.merge(
|
57
|
+
key: key,
|
58
|
+
attribute: attribute,
|
59
|
+
bidx_attribute: bidx_attribute,
|
60
|
+
migrating: migrating
|
61
|
+
)
|
62
|
+
|
63
|
+
define_singleton_method class_method_name do |value|
|
64
|
+
BlindIndex.generate_bidx(value, blind_indexes[name])
|
65
|
+
end
|
66
|
+
|
67
|
+
define_singleton_method method_name do |value|
|
68
|
+
ActiveSupport::Deprecation.warn("Use #{class_method_name} instead")
|
69
|
+
send(class_method_name, value)
|
70
|
+
end
|
71
|
+
|
72
|
+
define_method method_name do
|
73
|
+
self.send("#{bidx_attribute}=", self.class.send(class_method_name, send(attribute)))
|
74
|
+
end
|
75
|
+
|
76
|
+
if callback
|
77
|
+
before_validation method_name, if: -> { changes.key?(attribute.to_s) }
|
78
|
+
end
|
79
|
+
|
80
|
+
# use include so user can override
|
81
|
+
include InstanceMethods if blind_indexes.size == 1
|
82
|
+
end
|
58
83
|
end
|
59
84
|
end
|
60
85
|
end
|
data/lib/blind_index/version.rb
CHANGED
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.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-07-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -16,14 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '5'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: argon2
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: bundler
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -98,16 +112,16 @@ dependencies:
|
|
98
112
|
name: sqlite3
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
100
114
|
requirements:
|
101
|
-
- - "
|
115
|
+
- - ">="
|
102
116
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
117
|
+
version: '0'
|
104
118
|
type: :development
|
105
119
|
prerelease: false
|
106
120
|
version_requirements: !ruby/object:Gem::Requirement
|
107
121
|
requirements:
|
108
|
-
- - "
|
122
|
+
- - ">="
|
109
123
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
124
|
+
version: '0'
|
111
125
|
- !ruby/object:Gem::Dependency
|
112
126
|
name: scrypt
|
113
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -123,7 +137,7 @@ dependencies:
|
|
123
137
|
- !ruby/object:Gem::Version
|
124
138
|
version: '0'
|
125
139
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
140
|
+
name: benchmark-ips
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
128
142
|
requirements:
|
129
143
|
- - ">="
|
@@ -137,19 +151,19 @@ dependencies:
|
|
137
151
|
- !ruby/object:Gem::Version
|
138
152
|
version: '0'
|
139
153
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
154
|
+
name: lockbox
|
141
155
|
requirement: !ruby/object:Gem::Requirement
|
142
156
|
requirements:
|
143
157
|
- - ">="
|
144
158
|
- !ruby/object:Gem::Version
|
145
|
-
version: '0'
|
159
|
+
version: '0.2'
|
146
160
|
type: :development
|
147
161
|
prerelease: false
|
148
162
|
version_requirements: !ruby/object:Gem::Requirement
|
149
163
|
requirements:
|
150
164
|
- - ">="
|
151
165
|
- !ruby/object:Gem::Version
|
152
|
-
version: '0'
|
166
|
+
version: '0.2'
|
153
167
|
description:
|
154
168
|
email: andrew@chartkick.com
|
155
169
|
executables: []
|
@@ -161,6 +175,7 @@ files:
|
|
161
175
|
- README.md
|
162
176
|
- lib/blind_index.rb
|
163
177
|
- lib/blind_index/extensions.rb
|
178
|
+
- lib/blind_index/key_generator.rb
|
164
179
|
- lib/blind_index/model.rb
|
165
180
|
- lib/blind_index/version.rb
|
166
181
|
homepage: https://github.com/ankane/blind_index
|
@@ -175,14 +190,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
175
190
|
requirements:
|
176
191
|
- - ">="
|
177
192
|
- !ruby/object:Gem::Version
|
178
|
-
version: '2.
|
193
|
+
version: '2.4'
|
179
194
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
180
195
|
requirements:
|
181
196
|
- - ">="
|
182
197
|
- !ruby/object:Gem::Version
|
183
198
|
version: '0'
|
184
199
|
requirements: []
|
185
|
-
rubygems_version: 3.0.
|
200
|
+
rubygems_version: 3.0.4
|
186
201
|
signing_key:
|
187
202
|
specification_version: 4
|
188
203
|
summary: Securely search encrypted database fields
|