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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +71 -54
- data/lib/blind_index.rb +28 -10
- data/lib/blind_index/model.rb +4 -8
- data/lib/blind_index/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a5e0cf17996a671847cd6b7d9c89820c4dfa319c1f6e8c60bff430d5fdbb222
|
4
|
+
data.tar.gz: 82d7b94d12cd4391d48d845613a75740c0edbcb38d9d8272054b3bea58574c81
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff9d3d7b14c867a1972faa2600bfec2918fee21b2c2108bfa5a358990d1fc661c2aab84cc39c1610a0064cfd6a9b0ad461c1269b4bfdc8b85414f944235d864b
|
7
|
+
data.tar.gz: 87f28352c529b0cb61a0b4cf050a3619234da3e21fb06b5ba36d3d605119910678947994d381bb3a1bad81716300fe184d0adf8aa73cb78a95bd93ce93a23206
|
data/CHANGELOG.md
CHANGED
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
|
7
|
+
Here’s a [full example](https://ankane.org/securing-user-emails-in-rails) of how to use it
|
8
8
|
|
9
9
|
[](https://travis-ci.org/ankane/blind_index)
|
10
10
|
|
11
11
|
## How It Works
|
12
12
|
|
13
|
-
We use [this approach](https://
|
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
|
-
##
|
15
|
+
## Installation
|
16
16
|
|
17
|
-
Add
|
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
|
-
|
23
|
+
## Getting Started
|
25
24
|
|
26
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
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-
|
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.
|
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:
|
168
|
+
blind_index :email, iterations: 100000, ...
|
165
169
|
end
|
166
170
|
```
|
167
171
|
|
168
|
-
|
172
|
+
> Changing this requires you to recompute the blind index.
|
173
|
+
|
174
|
+
### Argon2
|
169
175
|
|
170
|
-
|
176
|
+
Argon2 is the state-of-the-art algorithm and recommended for best security.
|
171
177
|
|
172
|
-
|
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: :
|
182
|
+
blind_index :email, algorithm: :argon2, ...
|
177
183
|
end
|
178
184
|
```
|
179
185
|
|
180
|
-
|
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: :
|
190
|
+
blind_index :email, algorithm: :argon2, cost: {t: 4, m: 15}, ...
|
185
191
|
end
|
186
192
|
```
|
187
193
|
|
188
|
-
|
194
|
+
> Changing this requires you to recompute the blind index.
|
189
195
|
|
190
|
-
|
196
|
+
The variant used is Argon2i.
|
191
197
|
|
192
|
-
|
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
|
-
|
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.
|
data/lib/blind_index.rb
CHANGED
@@ -13,7 +13,7 @@ module BlindIndex
|
|
13
13
|
end
|
14
14
|
self.default_options = {
|
15
15
|
iterations: 10000,
|
16
|
-
algorithm: :
|
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 == :
|
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
|
52
|
-
when :
|
53
|
-
t = cost_options[:t] || 3
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
76
|
+
OpenSSL::PKCS5.pbkdf2_hmac(value, key, iterations, size, "sha256")
|
59
77
|
else
|
60
78
|
raise BlindIndex::Error, "Unknown algorithm"
|
61
79
|
end
|
data/lib/blind_index/model.rb
CHANGED
@@ -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
|
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
|
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.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
|
+
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.
|
186
|
+
rubygems_version: 2.7.6
|
187
187
|
signing_key:
|
188
188
|
specification_version: 4
|
189
189
|
summary: Securely search encrypted database fields
|