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 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