blind_index 0.2.1 → 0.3.0
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 +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
|
[![Build Status](https://travis-ci.org/ankane/blind_index.svg?branch=master)](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
|