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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c05a4761d6d0ea7b3b509af0f470323b2dad0acf50b8e1278bf0c65ae77bce3
4
- data.tar.gz: bc9acee38ec03827dde2faacd9f0fc83c6a1900888dbe472d3d1e3b5971c532d
3
+ metadata.gz: 651caa81ae68cdce70f5f0f83d3f2b1d0d2365c2c34765414d3ae0e523e5cdab
4
+ data.tar.gz: 4a7d274aec56ba615a9569ab19c222b24d4a052166e8acdc0bfd2a5c65b4855b
5
5
  SHA512:
6
- metadata.gz: 53f9fb4eea7741c64a353e14b229a7c176bcd80d156e691a727477508aa995785611da57adf3c01d553415b39061798831a3ab49988cf4ef43a7343ead42f446
7
- data.tar.gz: 4671cdb7b7f3c33476bf629ab406e35496db7a5075c0da0b5f5190ae50c68447bdff68299b151072d3118569715dd84d9b7ff0fd4ad1056c84bb7af6b58b45eb
6
+ metadata.gz: e169875cc33b23da106311fbbc41a2647bbd6b3abfd5e904f9ad7315ea71117d2dd69cc829ff16b222640397ba5576d12094fe2a62000e9c6962409dc6ccd478
7
+ data.tar.gz: a7ee41ba497568c4580d98508c204f956aff7ed1ee04bb4452b78fe2897e98015ec3e9718bc6ef174b432912bfb7cf55bd94db79c45e14041ad057f04a49055f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.3.0
2
+
3
+ - Enforce secure key generation
4
+ - Added `encode` option
5
+ - Added `default_options` method
6
+
1
7
  ## 0.2.1
2
8
 
3
9
  - Added class method to compute blind index
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. For development, you can use these:
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=00000000000000000000000000000000
47
- EMAIL_BLIND_INDEX_KEY=99999999999999999999999999999999
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
- ## Key Stretching
110
+ ## Index Only
103
111
 
104
- 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:
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
- If you don’t need to store the original value (for instance, when just checking duplicates), use a virtual attribute:
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
- attribute :email
121
- blind_index :email, ...
155
+ blind_index :email, algorithm: :scrypt, ...
122
156
  end
123
157
  ```
124
158
 
125
- ## Fixtures
159
+ ### Argon2
126
160
 
127
- You can use blind indexes in fixtures with:
161
+ :warning: *Not production ready yet*
128
162
 
129
- ```yml
130
- test_user:
131
- encrypted_email_bidx: <%= User.compute_email_bidx("test@example.org").inspect %>
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
- def self.generate_bidx(value, key:, iterations:, expression: nil, **options)
12
- key = key.call if key.respond_to?(:call)
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
- raise BlindIndex::Error, "Missing key for blind index" unless key
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
- # generate hash
21
- digest = OpenSSL::Digest::SHA256.new
22
- value = OpenSSL::PKCS5.pbkdf2_hmac(value.to_s, key, iterations, digest.digest_length, digest)
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
- # encode
25
- [value].pack("m")
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
@@ -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])
@@ -1,3 +1,3 @@
1
1
  module BlindIndex
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
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.2.1
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-05-26 00:00:00.000000000 Z
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