blind_index 0.2.1 → 0.3.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 +6 -0
- data/README.md +97 -17
- data/Rakefile +17 -0
- data/blind_index.gemspec +3 -0
- data/lib/blind_index.rb +49 -9
- data/lib/blind_index/model.rb +6 -3
- data/lib/blind_index/version.rb +1 -1
- metadata +44 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 651caa81ae68cdce70f5f0f83d3f2b1d0d2365c2c34765414d3ae0e523e5cdab
|
4
|
+
data.tar.gz: 4a7d274aec56ba615a9569ab19c222b24d4a052166e8acdc0bfd2a5c65b4855b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e169875cc33b23da106311fbbc41a2647bbd6b3abfd5e904f9ad7315ea71117d2dd69cc829ff16b222640397ba5576d12094fe2a62000e9c6962409dc6ccd478
|
7
|
+
data.tar.gz: a7ee41ba497568c4580d98508c204f956aff7ed1ee04bb4452b78fe2897e98015ec3e9718bc6ef174b432912bfb7cf55bd94db79c45e14041ad057f04a49055f
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -4,11 +4,13 @@ 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://shorts.dokkuapp.com/securing-user-emails-in-rails/) of how to use it with Devise
|
8
|
+
|
7
9
|
[](https://travis-ci.org/ankane/blind_index)
|
8
10
|
|
9
11
|
## How It Works
|
10
12
|
|
11
|
-
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) 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://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.
|
12
14
|
|
13
15
|
## Getting Started
|
14
16
|
|
@@ -35,16 +37,22 @@ And add to your model
|
|
35
37
|
|
36
38
|
```ruby
|
37
39
|
class User < ApplicationRecord
|
38
|
-
attr_encrypted :email, key: ENV["EMAIL_ENCRYPTION_KEY"]
|
39
|
-
blind_index :email, key: ENV["EMAIL_BLIND_INDEX_KEY"]
|
40
|
+
attr_encrypted :email, key: [ENV["EMAIL_ENCRYPTION_KEY"]].pack("H*")
|
41
|
+
blind_index :email, key: [ENV["EMAIL_BLIND_INDEX_KEY"]].pack("H*")
|
40
42
|
end
|
41
43
|
```
|
42
44
|
|
43
|
-
We use environment variables to store the keys ([dotenv](https://github.com/bkeepers/dotenv) is great for this). *Do not commit them to source control.* Generate one key for encryption and one key for hashing.
|
45
|
+
We use environment variables to store the keys ([dotenv](https://github.com/bkeepers/dotenv) is great for this). *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:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
SecureRandom.hex(32)
|
49
|
+
```
|
50
|
+
|
51
|
+
For development, you can use these:
|
44
52
|
|
45
53
|
```sh
|
46
|
-
EMAIL_ENCRYPTION_KEY=
|
47
|
-
EMAIL_BLIND_INDEX_KEY=
|
54
|
+
EMAIL_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
55
|
+
EMAIL_BLIND_INDEX_KEY=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
|
48
56
|
```
|
49
57
|
|
50
58
|
And query away
|
@@ -99,9 +107,33 @@ Search with:
|
|
99
107
|
User.where(email_ci: "test@example.org")
|
100
108
|
```
|
101
109
|
|
102
|
-
##
|
110
|
+
## Index Only
|
103
111
|
|
104
|
-
|
112
|
+
If you don’t need to store the original value (for instance, when just checking duplicates), use a virtual attribute:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
class User < ApplicationRecord
|
116
|
+
attribute :email
|
117
|
+
blind_index :email, ...
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
121
|
+
## Fixtures
|
122
|
+
|
123
|
+
You can use blind indexes in fixtures with:
|
124
|
+
|
125
|
+
```yml
|
126
|
+
test_user:
|
127
|
+
encrypted_email_bidx: <%= User.compute_email_bidx("test@example.org").inspect %>
|
128
|
+
```
|
129
|
+
|
130
|
+
Be sure to include the `inspect` at the end, or it won’t be encoded properly in YAML.
|
131
|
+
|
132
|
+
## Algorithms
|
133
|
+
|
134
|
+
### PBKDF2-HMAC-SHA256
|
135
|
+
|
136
|
+
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:
|
105
137
|
|
106
138
|
```ruby
|
107
139
|
class User < ApplicationRecord
|
@@ -111,24 +143,72 @@ end
|
|
111
143
|
|
112
144
|
The default is `10000`. Changing this value requires you to recompute the blind index.
|
113
145
|
|
114
|
-
## Index Only
|
115
146
|
|
116
|
-
|
147
|
+
### scrypt
|
148
|
+
|
149
|
+
:warning: *Not production ready yet*
|
150
|
+
|
151
|
+
Add [scrypt](https://github.com/pbhogan/scrypt) to your Gemfile and use:
|
117
152
|
|
118
153
|
```ruby
|
119
154
|
class User < ApplicationRecord
|
120
|
-
|
121
|
-
blind_index :email, ...
|
155
|
+
blind_index :email, algorithm: :scrypt, ...
|
122
156
|
end
|
123
157
|
```
|
124
158
|
|
125
|
-
|
159
|
+
### Argon2
|
126
160
|
|
127
|
-
|
161
|
+
:warning: *Not production ready yet*
|
128
162
|
|
129
|
-
|
130
|
-
|
131
|
-
|
163
|
+
Add [argon2](https://github.com/technion/ruby-argon2) to your Gemfile and use:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
class User < ApplicationRecord
|
167
|
+
blind_index :email, algorithm: :argon2, ...
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
## Reference
|
172
|
+
|
173
|
+
By default, blind indexes are encoded in Base64. Set a different encoding with:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
class User < ApplicationRecord
|
177
|
+
blind_index :email, encode: ->(v) { [v].pack("H*") }
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
## Alternatives
|
182
|
+
|
183
|
+
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.
|
184
|
+
|
185
|
+
## Upgrading
|
186
|
+
|
187
|
+
### 0.3.0
|
188
|
+
|
189
|
+
This version introduces a breaking change to enforce secure key generation. An error is thrown if your blind index key isn’t both binary and 32 bytes.
|
190
|
+
|
191
|
+
We recommend rotating your key if it doesn’t meet this criteria. You can generate a new key in the Rails console with:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
SecureRandom.hex(32)
|
195
|
+
```
|
196
|
+
|
197
|
+
Set the new key and recompute the blind index.
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
User.find_each do |user|
|
201
|
+
user.compute_email_bidx
|
202
|
+
user.save!
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
To continue without rotating, set:
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
class User < ApplicationRecord
|
210
|
+
blind_index :email, insecure_key: true, ...
|
211
|
+
end
|
132
212
|
```
|
133
213
|
|
134
214
|
## History
|
data/Rakefile
CHANGED
@@ -9,3 +9,20 @@ Rake::TestTask.new(:test) do |t|
|
|
9
9
|
end
|
10
10
|
|
11
11
|
task default: :test
|
12
|
+
|
13
|
+
task :benchmark do
|
14
|
+
require "securerandom"
|
15
|
+
require "benchmark/ips"
|
16
|
+
require "blind_index"
|
17
|
+
require "scrypt"
|
18
|
+
require "argon2"
|
19
|
+
|
20
|
+
key = SecureRandom.random_bytes(32)
|
21
|
+
value = "secret"
|
22
|
+
|
23
|
+
Benchmark.ips do |x|
|
24
|
+
x.report("pbkdf2_hmac") { BlindIndex.generate_bidx(value, key: key, algorithm: :pbkdf2_hmac) }
|
25
|
+
x.report("scrypt") { BlindIndex.generate_bidx(value, key: key, algorithm: :scrypt) }
|
26
|
+
x.report("argon2") { BlindIndex.generate_bidx(value, key: key, algorithm: :argon2) }
|
27
|
+
end
|
28
|
+
end
|
data/blind_index.gemspec
CHANGED
@@ -28,4 +28,7 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.add_development_dependency "attr_encrypted"
|
29
29
|
spec.add_development_dependency "activerecord"
|
30
30
|
spec.add_development_dependency "sqlite3"
|
31
|
+
spec.add_development_dependency "scrypt"
|
32
|
+
spec.add_development_dependency "argon2"
|
33
|
+
spec.add_development_dependency "benchmark-ips"
|
31
34
|
end
|
data/lib/blind_index.rb
CHANGED
@@ -8,21 +8,61 @@ require "blind_index/version"
|
|
8
8
|
module BlindIndex
|
9
9
|
class Error < StandardError; end
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
class << self
|
12
|
+
attr_accessor :default_options
|
13
|
+
end
|
14
|
+
self.default_options = {
|
15
|
+
iterations: 10000,
|
16
|
+
algorithm: :pbkdf2_hmac,
|
17
|
+
insecure_key: false,
|
18
|
+
encode: true
|
19
|
+
}
|
13
20
|
|
14
|
-
|
21
|
+
def self.generate_bidx(value, key:, **options)
|
22
|
+
options = default_options.merge(options)
|
15
23
|
|
16
24
|
# apply expression
|
17
|
-
value = expression.call(value) if expression
|
25
|
+
value = options[:expression].call(value) if options[:expression]
|
18
26
|
|
19
27
|
unless value.nil?
|
20
|
-
|
21
|
-
|
22
|
-
|
28
|
+
algorithm = options[:algorithm].to_sym
|
29
|
+
|
30
|
+
key = key.call if key.respond_to?(:call)
|
31
|
+
raise BlindIndex::Error, "Missing key for blind index" unless key
|
32
|
+
|
33
|
+
key = key.to_s
|
34
|
+
unless options[:insecure_key] && algorithm == :pbkdf2_hmac
|
35
|
+
raise BlindIndex::Error, "Key must use binary encoding" if key.encoding != Encoding::BINARY
|
36
|
+
# raise BlindIndex::Error, "Key must not be ASCII" if key.bytes.all? { |b| b < 128 }
|
37
|
+
raise BlindIndex::Error, "Key must be 32 bytes" if key.bytesize != 32
|
38
|
+
end
|
39
|
+
|
40
|
+
# gist to compare algorithm results
|
41
|
+
# https://gist.github.com/ankane/fe3ac63fbf1c4550ee12554c664d2b8c
|
42
|
+
value =
|
43
|
+
case algorithm
|
44
|
+
when :scrypt
|
45
|
+
# n, p (keep r at 8)
|
46
|
+
SCrypt::Engine.scrypt(value.to_s, key, 4096, 8, 1, 32)
|
47
|
+
when :argon2
|
48
|
+
# t_cost, m_cost
|
49
|
+
[Argon2::Engine.hash_argon2i(value.to_s, key, 2, 12)].pack("H*")
|
50
|
+
when :pbkdf2_hmac
|
51
|
+
OpenSSL::PKCS5.pbkdf2_hmac(value.to_s, key, options[:iterations], 32, "sha256")
|
52
|
+
else
|
53
|
+
raise BlindIndex::Error, "Unknown algorithm"
|
54
|
+
end
|
23
55
|
|
24
|
-
|
25
|
-
|
56
|
+
encode = options[:encode]
|
57
|
+
if encode
|
58
|
+
if encode.respond_to?(:call)
|
59
|
+
encode.call(value)
|
60
|
+
else
|
61
|
+
[value].pack("m")
|
62
|
+
end
|
63
|
+
else
|
64
|
+
value
|
65
|
+
end
|
26
66
|
end
|
27
67
|
end
|
28
68
|
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)
|
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)
|
4
4
|
iterations ||= 10000
|
5
5
|
attribute ||= name
|
6
6
|
bidx_attribute ||= :"encrypted_#{name}_bidx"
|
@@ -23,8 +23,11 @@ module BlindIndex
|
|
23
23
|
iterations: iterations,
|
24
24
|
attribute: attribute,
|
25
25
|
expression: expression,
|
26
|
-
bidx_attribute: bidx_attribute
|
27
|
-
|
26
|
+
bidx_attribute: bidx_attribute,
|
27
|
+
algorithm: algorithm,
|
28
|
+
insecure_key: insecure_key,
|
29
|
+
encode: encode
|
30
|
+
}.reject { |_, v| v.nil? }
|
28
31
|
|
29
32
|
define_singleton_method method_name do |value|
|
30
33
|
BlindIndex.generate_bidx(value, blind_indexes[name])
|
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: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-06-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -108,6 +108,48 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: scrypt
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: argon2
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: benchmark-ips
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
111
153
|
description:
|
112
154
|
email:
|
113
155
|
- andrew@chartkick.com
|