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 +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
|
[![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://
|
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
|