blind_index 1.0.1 → 2.1.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: 93b1688db3ff1410b5e56fcf3fae01d76cfcb073df3647abb6582fffbb05d148
4
- data.tar.gz: 3b0b3dff637c2eab2b7b4d5811b413166cd23503dec1d6081fb5bfee7533ce99
3
+ metadata.gz: f6749c94ee9432d60e3095e77a3948f480c345b793555dadf5b25261247e406b
4
+ data.tar.gz: e29eb3a994de26b1f67cb31a16bbaef66742aea0cf366ea5830247421c30a5b0
5
5
  SHA512:
6
- metadata.gz: fcae62db34e27e82885d6fb200b6d3549bc14d9c7c1ddedf9d59aa32a7f9691010aa641379318fbb20706823b258328013ba7eb3cc2b6e63242ae4addcdc7a0e
7
- data.tar.gz: 45a07059d4d78d2a9f814db37d9a80fe37603e78603207b65f80a812bce6e18c818ebb0c3fc7e6b8439203eec4398a81a9db7b32bfaa7061865244841050ad2c
6
+ metadata.gz: 4a195bd440c17774dcaf46196c34f2965a78ef7a95499501534fba3161cafc82af085a0046f9bc5e422e7b38476040c389ae3440cf0589db190cd9240f4c6521
7
+ data.tar.gz: b1ba60817538b3fda557a392e136a2a61c80341332f16821a36e60b8f573659574fbfe36d85969f7730b172451de5e0c5a369d6a4a11c0e65608cfe369229c37
@@ -1,8 +1,34 @@
1
- ## 1.0.1
1
+ ## 2.1.0 (2020-07-06)
2
+
3
+ - Improved performance of uniqueness validations
4
+ - Fixed deprecation warnings in Ruby 2.7 with Mongoid
5
+
6
+ ## 2.0.2 (2020-06-01)
7
+
8
+ - Improved error message for bad key length
9
+ - Fixed `backfill` method with relations for Mongoid
10
+
11
+ ## 2.0.1 (2020-02-14)
12
+
13
+ - Added `BlindIndex.backfill` method
14
+
15
+ ## 2.0.0 (2019-02-10)
16
+
17
+ - Blind indexes are updated immediately instead of in a `before_validation` callback
18
+ - Better Lockbox integration - no need to generate a separate key
19
+ - The `argon2` gem has been replaced with `argon2-kdf` for less dependencies and Windows support
20
+ - Removed deprecated `compute_email_bidx`
21
+
22
+ ## 1.0.2 (2019-12-26)
23
+
24
+ - Fixed `OpenSSL::KDF` error on some platforms
25
+ - Fixed deprecation warnings in Ruby 2.7
26
+
27
+ ## 1.0.1 (2019-08-16)
2
28
 
3
29
  - Added support for Mongoid
4
30
 
5
- ## 1.0.0
31
+ ## 1.0.0 (2019-07-08)
6
32
 
7
33
  - Added support for master key
8
34
  - Added support for Argon2id
@@ -15,56 +41,56 @@ Breaking changes
15
41
  - Removed `encrypted_` prefix from columns
16
42
  - Changed default encoding to Base64 strict
17
43
 
18
- ## 0.3.5
44
+ ## 0.3.5 (2019-05-28)
19
45
 
20
46
  - Added support for hex keys
21
47
  - Added `generate_key` method
22
48
  - Fixed querying with array values
23
49
 
24
- ## 0.3.4
50
+ ## 0.3.4 (2018-12-16)
25
51
 
26
52
  - Added `size` option
27
53
  - Added sanity checks for Argon2 cost parameters
28
54
  - Fixed ActiveRecord callback issues introduced in 0.3.3
29
55
 
30
- ## 0.3.3
56
+ ## 0.3.3 (2018-11-12)
31
57
 
32
58
  - Added support for string keys in finders
33
59
 
34
- ## 0.3.2
60
+ ## 0.3.2 (2018-06-18)
35
61
 
36
62
  - Added support for dynamic finders
37
63
  - Added support for inherited models
38
64
 
39
- ## 0.3.1
65
+ ## 0.3.1 (2018-06-04)
40
66
 
41
67
  - Added scrypt and Argon2 algorithms
42
68
  - Added `cost` option
43
69
 
44
- ## 0.3.0
70
+ ## 0.3.0 (2018-06-03)
45
71
 
46
72
  - Enforce secure key generation
47
73
  - Added `encode` option
48
74
  - Added `default_options` method
49
75
 
50
- ## 0.2.1
76
+ ## 0.2.1 (2018-05-26)
51
77
 
52
78
  - Added class method to compute blind index
53
79
  - Fixed issue with cached statements
54
80
 
55
- ## 0.2.0
81
+ ## 0.2.0 (2018-05-11)
56
82
 
57
83
  - Added support for ActiveRecord 4.2
58
84
  - Improved validation support when multiple blind indexes
59
85
  - Fixed `nil` handling
60
86
 
61
- ## 0.1.1
87
+ ## 0.1.1 (2018-04-09)
62
88
 
63
89
  - Added support for ActiveRecord 5.2
64
90
  - Added `callback` option
65
91
  - Added support for `key` proc
66
92
  - Fixed error inheritance
67
93
 
68
- ## 0.1.0
94
+ ## 0.1.0 (2017-12-17)
69
95
 
70
96
  - First release
@@ -1,4 +1,4 @@
1
- Copyright (c) 2017-2019 Andrew Kane
1
+ Copyright (c) 2017-2020 Andrew Kane
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -16,7 +16,7 @@ We use [this approach](https://paragonie.com/blog/2017/05/building-searchable-en
16
16
 
17
17
  An important consideration in searchable encryption is leakage, which is information an attacker can gain. Blind indexing leaks that rows have the same value. If you use this for a field like last name, an attacker can use frequency analysis to predict the values. In an active attack where an attacker can control the input values, they can learn which other values in the database match.
18
18
 
19
- Here’s a [great article](https://blog.cryptographyengineering.com/2019/02/11/attack-of-the-week-searchable-encryption-and-the-ever-expanding-leakage-function/) on leakage in searchable encryption. Blind indexing has the same leakage as deterministic encryption.
19
+ Here’s a [great article](https://blog.cryptographyengineering.com/2019/02/11/attack-of-the-week-searchable-encryption-and-the-ever-expanding-leakage-function/) on leakage in searchable encryption. Blind indexing has the same leakage as [deterministic encryption](#alternatives).
20
20
 
21
21
  ## Installation
22
22
 
@@ -26,38 +26,14 @@ Add this line to your application’s Gemfile:
26
26
  gem 'blind_index'
27
27
  ```
28
28
 
29
- On Windows, also add:
29
+ ## Prep
30
30
 
31
- ```ruby
32
- gem 'argon2', git: 'https://github.com/technion/ruby-argon2.git', submodules: true
33
- ```
31
+ Your model should already be set up with Lockbox or attr_encrypted. The examples are for a `User` model with `encrypts :email` or `attr_encrypted :email`. See the full examples for [Lockbox](https://ankane.org/securing-user-emails-lockbox) and [attr_encrypted](https://ankane.org/securing-user-emails-in-rails) if needed.
34
32
 
35
- Until `argon2 > 2.0.2` is released.
33
+ Also, if you use attr_encrypted, [generate a key](#key-generation).
36
34
 
37
35
  ## Getting Started
38
36
 
39
- > Note: Your model should already be set up with Lockbox or attr_encrypted. The examples are for a `User` model with `encrypts :email` or `attr_encrypted :email`. See the full examples for [Lockbox](https://ankane.org/securing-user-emails-lockbox) and [attr_encrypted](https://ankane.org/securing-user-emails-in-rails) if needed.
40
-
41
- First, generate a key
42
-
43
- ```ruby
44
- BlindIndex.generate_key
45
- ```
46
-
47
- Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this). Be sure to use different keys in development and production. Keys don’t need to be hex-encoded, but it’s often easier to store them this way.
48
-
49
- Set the following environment variable with your key (you can use this one in development)
50
-
51
- ```sh
52
- BLIND_INDEX_MASTER_KEY=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
53
- ```
54
-
55
- or create `config/initializers/blind_index.rb` with something like
56
-
57
- ```ruby
58
- BlindIndex.master_key = Rails.application.credentials.blind_index_master_key
59
- ```
60
-
61
37
  Create a migration to add a column for the blind index
62
38
 
63
39
  ```ruby
@@ -84,10 +60,7 @@ end
84
60
  Backfill existing records
85
61
 
86
62
  ```ruby
87
- User.unscoped.where(email_bidx: nil).find_each do |user|
88
- user.compute_email_bidx
89
- user.save(validate: false)
90
- end
63
+ BlindIndex.backfill(User)
91
64
  ```
92
65
 
93
66
  And query away
@@ -96,9 +69,19 @@ And query away
96
69
  User.where(email: "test@example.org")
97
70
  ```
98
71
 
72
+ ## Expressions
73
+
74
+ You can apply expressions to attributes before indexing and searching. This gives you the the ability to perform case-insensitive searches and more.
75
+
76
+ ```ruby
77
+ class User < ApplicationRecord
78
+ blind_index :email, expression: ->(v) { v.downcase }
79
+ end
80
+ ```
81
+
99
82
  ## Validations
100
83
 
101
- To prevent duplicates, use:
84
+ You can use blind indexes for uniqueness validations.
102
85
 
103
86
  ```ruby
104
87
  class User < ApplicationRecord
@@ -106,15 +89,27 @@ class User < ApplicationRecord
106
89
  end
107
90
  ```
108
91
 
109
- We also recommend adding a unique index to the blind index column through a database migration.
92
+ We recommend adding a unique index to the blind index column through a database migration.
110
93
 
111
- ## Expressions
94
+ ```ruby
95
+ add_index :users, :email_bidx, unique: true
96
+ ```
112
97
 
113
- You can apply expressions to attributes before indexing and searching. This gives you the the ability to perform case-insensitive searches and more.
98
+ For `allow_blank: true`, use:
99
+
100
+ ```ruby
101
+ class User < ApplicationRecord
102
+ blind_index :email, expression: ->(v) { v.presence }
103
+ validates :email, uniqueness: {allow_blank: true}
104
+ end
105
+ ```
106
+
107
+ For `case_sensitive: false`, use:
114
108
 
115
109
  ```ruby
116
110
  class User < ApplicationRecord
117
111
  blind_index :email, expression: ->(v) { v.downcase }
112
+ validates :email, uniqueness: true # for best performance, leave out {case_sensitive: false}
118
113
  end
119
114
  ```
120
115
 
@@ -139,10 +134,7 @@ end
139
134
  Backfill existing records
140
135
 
141
136
  ```ruby
142
- User.unscoped.where(email_ci_bidx: nil).find_each do |user|
143
- user.compute_email_ci_bidx
144
- user.save(validate: false)
145
- end
137
+ BlindIndex.backfill(User, columns: [:email_ci_bidx])
146
138
  ```
147
139
 
148
140
  And query away
@@ -169,18 +161,34 @@ You can also use virtual attributes to index data from multiple columns:
169
161
  ```ruby
170
162
  class User < ApplicationRecord
171
163
  attribute :initials, :string
164
+ blind_index :initials
172
165
 
173
- # must come before the blind_index method so it runs first
174
166
  before_validation :set_initials, if: -> { changes.key?(:first_name) || changes.key?(:last_name) }
175
167
 
176
- blind_index :initials
177
-
178
168
  def set_initials
179
169
  self.initials = "#{first_name[0]}#{last_name[0]}"
180
170
  end
181
171
  end
182
172
  ```
183
173
 
174
+ ## Migrating Data
175
+
176
+ If you’re encrypting a column and adding a blind index at the same time, use the `migrating` option.
177
+
178
+ ```ruby
179
+ class User < ApplicationRecord
180
+ blind_index :email, migrating: true
181
+ end
182
+ ```
183
+
184
+ This allows you to backfill records while still querying the unencrypted field.
185
+
186
+ ```ruby
187
+ BlindIndex.backfill(User)
188
+ ```
189
+
190
+ Once that completes, you can remove the `migrating` option.
191
+
184
192
  ## Key Rotation
185
193
 
186
194
  To rotate keys without downtime, add a new column:
@@ -201,10 +209,7 @@ end
201
209
  This will keep the new column synced going forward. Next, backfill the data:
202
210
 
203
211
  ```ruby
204
- User.unscoped.where(email_bidx_v2: nil).find_each do |user|
205
- user.compute_rotated_email_bidx
206
- user.save(validate: false)
207
- end
212
+ BlindIndex.backfill(User, columns: [:email_bidx_v2])
208
213
  ```
209
214
 
210
215
  Then update your model
@@ -260,6 +265,30 @@ class User
260
265
  end
261
266
  ```
262
267
 
268
+ ## Key Generation
269
+
270
+ This is optional for Lockbox, as its master key is used by default.
271
+
272
+ Generate a key with:
273
+
274
+ ```ruby
275
+ BlindIndex.generate_key
276
+ ```
277
+
278
+ Store the key with your other secrets. This is typically Rails credentials or an environment variable ([dotenv](https://github.com/bkeepers/dotenv) is great for this). Be sure to use different keys in development and production. Keys don’t need to be hex-encoded, but it’s often easier to store them this way.
279
+
280
+ Set the following environment variable with your key (you can use this one in development)
281
+
282
+ ```sh
283
+ BLIND_INDEX_MASTER_KEY=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
284
+ ```
285
+
286
+ or create `config/initializers/blind_index.rb` with something like
287
+
288
+ ```ruby
289
+ BlindIndex.master_key = Rails.application.credentials.blind_index_master_key
290
+ ```
291
+
263
292
  ## Reference
264
293
 
265
294
  Set default options in an initializer with:
@@ -294,10 +323,21 @@ end
294
323
 
295
324
  ## Alternatives
296
325
 
297
- 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.
326
+ 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. We recommend blind indexing over deterministic encryption because:
327
+
328
+ 1. You can keep encryption consistent for all fields (both searchable and non-searchable)
329
+ 2. Blind indexing supports expressions
298
330
 
299
331
  ## Upgrading
300
332
 
333
+ ### 2.0.0
334
+
335
+ 2.0.0 brings a number of improvements.
336
+
337
+ - Blind indexes are updated immediately instead of in a `before_validation` callback
338
+ - Better Lockbox integration - no need to generate a separate key
339
+ - There’s a new gem for Argon2 that has no dependencies and (officially) supports Windows
340
+
301
341
  ### 1.0.0
302
342
 
303
343
  1.0.0 brings a number of improvements. Here are a few to be aware of:
@@ -335,7 +375,7 @@ And add to your model
335
375
 
336
376
  ```ruby
337
377
  class User < ApplicationRecord
338
- blind_index :email, key: ENV["USER_EMAIL_BLIND_INDEX_KEY"], legacy: true, rotate: true
378
+ blind_index :email, key: ENV["USER_EMAIL_BLIND_INDEX_KEY"], legacy: true, rotate: {}
339
379
  end
340
380
  ```
341
381
 
@@ -416,5 +456,7 @@ To get started with development and testing:
416
456
  git clone https://github.com/ankane/blind_index.git
417
457
  cd blind_index
418
458
  bundle install
419
- rake test
459
+ bundle exec rake test
420
460
  ```
461
+
462
+ For security issues, send an email to the address on [this page](https://github.com/ankane).
@@ -1,9 +1,10 @@
1
1
  # dependencies
2
2
  require "active_support"
3
3
  require "openssl"
4
- require "argon2"
4
+ require "argon2/kdf"
5
5
 
6
6
  # modules
7
+ require "blind_index/backfill"
7
8
  require "blind_index/key_generator"
8
9
  require "blind_index/model"
9
10
  require "blind_index/version"
@@ -18,7 +19,7 @@ module BlindIndex
18
19
  self.default_options = {}
19
20
 
20
21
  def self.master_key
21
- @master_key ||= ENV["BLIND_INDEX_MASTER_KEY"]
22
+ @master_key ||= ENV["BLIND_INDEX_MASTER_KEY"] || (defined?(Lockbox.master_key) && Lockbox.master_key)
22
23
  end
23
24
 
24
25
  def self.generate_bidx(value, key:, **options)
@@ -64,7 +65,7 @@ module BlindIndex
64
65
  # use same bounds as rbnacl
65
66
  raise BlindIndex::Error, "m must be between 3 and 22" if m < 3 || m > 22
66
67
 
67
- [Argon2::Engine.hash_argon2id(value, key, t, m, size)].pack("H*")
68
+ Argon2::KDF.argon2id(value, salt: key, t: t, m: m, p: 1, length: size)
68
69
  when :pbkdf2_sha256
69
70
  iterations = cost_options[:iterations] || options[:iterations] || (options[:slow] ? 100000 : 10000)
70
71
  OpenSSL::PKCS5.pbkdf2_hmac(value, key, iterations, size, "sha256")
@@ -78,7 +79,7 @@ module BlindIndex
78
79
  # use same bounds as rbnacl
79
80
  raise BlindIndex::Error, "m must be between 3 and 22" if m < 3 || m > 22
80
81
 
81
- [Argon2::Engine.hash_argon2i(value, key, t, m, size)].pack("H*")
82
+ Argon2::KDF.argon2i(value, salt: key, t: t, m: m, p: 1, length: size)
82
83
  when :scrypt
83
84
  n = cost_options[:n] || 4096
84
85
  r = cost_options[:r] || 8
@@ -116,29 +117,28 @@ module BlindIndex
116
117
  key
117
118
  end
118
119
 
119
- def self.decode_key(key)
120
+ def self.decode_key(key, name: "Key")
120
121
  # decode hex key
121
122
  if key.encoding != Encoding::BINARY && key =~ /\A[0-9a-f]{64}\z/i
122
123
  key = [key].pack("H*")
123
124
  end
124
125
 
125
- raise BlindIndex::Error, "Key must use binary encoding" if key.encoding != Encoding::BINARY
126
- raise BlindIndex::Error, "Key must be 32 bytes" if key.bytesize != 32
126
+ raise BlindIndex::Error, "#{name} must be 32 bytes (64 hex digits)" if key.bytesize != 32
127
+ raise BlindIndex::Error, "#{name} must use binary encoding" if key.encoding != Encoding::BINARY
127
128
 
128
129
  key
129
130
  end
131
+
132
+ def self.backfill(relation, columns: nil, batch_size: 1000)
133
+ Backfill.new(relation, columns: columns, batch_size: batch_size).perform
134
+ end
130
135
  end
131
136
 
132
137
  ActiveSupport.on_load(:active_record) do
133
138
  require "blind_index/extensions"
134
139
  extend BlindIndex::Model
135
140
 
136
- if defined?(ActiveRecord::TableMetadata)
137
- ActiveRecord::TableMetadata.prepend(BlindIndex::Extensions::TableMetadata)
138
- else
139
- ActiveRecord::PredicateBuilder.singleton_class.prepend(BlindIndex::Extensions::PredicateBuilder)
140
- end
141
-
141
+ ActiveRecord::TableMetadata.prepend(BlindIndex::Extensions::TableMetadata)
142
142
  ActiveRecord::DynamicMatchers::Method.prepend(BlindIndex::Extensions::DynamicMatchers)
143
143
 
144
144
  unless ActiveRecord::VERSION::STRING.start_with?("5.1.")
@@ -0,0 +1,113 @@
1
+ module BlindIndex
2
+ class Backfill
3
+ attr_reader :blind_indexes
4
+
5
+ def initialize(relation, batch_size:, columns:)
6
+ @relation = relation
7
+ @transaction = @relation.respond_to?(:transaction)
8
+ @batch_size = batch_size
9
+ @blind_indexes = @relation.blind_indexes
10
+ filter_columns!(columns) if columns
11
+ end
12
+
13
+ def perform
14
+ each_batch do |records|
15
+ backfill_records(records)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ # modify in-place
22
+ def filter_columns!(columns)
23
+ columns = Array(columns).map(&:to_s)
24
+ blind_indexes.select! { |_, v| columns.include?(v[:bidx_attribute]) }
25
+ bad_columns = columns - blind_indexes.map { |_, v| v[:bidx_attribute] }
26
+ raise ArgumentError, "Bad column: #{bad_columns.first}" if bad_columns.any?
27
+ end
28
+
29
+ def build_relation
30
+ # build relation
31
+ relation = @relation
32
+
33
+ if defined?(ActiveRecord::Base) && relation.is_a?(ActiveRecord::Base)
34
+ relation = relation.unscoped
35
+ end
36
+
37
+ # convert from possible class to ActiveRecord::Relation or Mongoid::Criteria
38
+ relation = relation.all
39
+
40
+ attributes = blind_indexes.map { |_, v| v[:bidx_attribute] }
41
+
42
+ if defined?(ActiveRecord::Relation) && relation.is_a?(ActiveRecord::Relation)
43
+ base_relation = relation.unscoped
44
+ or_relation = relation.unscoped
45
+
46
+ attributes.each_with_index do |attribute, i|
47
+ or_relation =
48
+ if i == 0
49
+ base_relation.where(attribute => nil)
50
+ else
51
+ or_relation.or(base_relation.where(attribute => nil))
52
+ end
53
+ end
54
+
55
+ relation.merge(or_relation)
56
+ else
57
+ relation.merge(relation.unscoped.or(attributes.map { |a| {a => nil} }))
58
+ end
59
+ end
60
+
61
+ def each_batch
62
+ relation = build_relation
63
+
64
+ if relation.respond_to?(:find_in_batches)
65
+ relation.find_in_batches(batch_size: @batch_size) do |records|
66
+ yield records
67
+ end
68
+ else
69
+ # https://github.com/karmi/tire/blob/master/lib/tire/model/import.rb
70
+ # use cursor for Mongoid
71
+ records = []
72
+ relation.all.each do |record|
73
+ records << record
74
+ if records.length == @batch_size
75
+ yield records
76
+ records = []
77
+ end
78
+ end
79
+ yield records if records.any?
80
+ end
81
+ end
82
+
83
+ def backfill_records(records)
84
+ # do expensive blind index computation outside of transaction
85
+ records.each do |record|
86
+ blind_indexes.each do |k, v|
87
+ record.send("compute_#{k}_bidx") if !record.send(v[:bidx_attribute])
88
+ end
89
+ end
90
+
91
+ # don't need to save records that went from nil => nil
92
+ records.select! { |r| r.changed? }
93
+
94
+ if records.any?
95
+ with_transaction do
96
+ records.each do |record|
97
+ record.save!(validate: false)
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def with_transaction
104
+ if @transaction
105
+ @relation.transaction do
106
+ yield
107
+ end
108
+ else
109
+ yield
110
+ end
111
+ end
112
+ end
113
+ end
@@ -1,6 +1,5 @@
1
1
  module BlindIndex
2
2
  module Extensions
3
- # ActiveRecord 5.0+
4
3
  module TableMetadata
5
4
  def resolve_column_aliases(hash)
6
5
  new_hash = super
@@ -10,9 +9,9 @@ module BlindIndex
10
9
  value = new_hash.delete(key)
11
10
  new_hash[bi[:bidx_attribute]] =
12
11
  if value.is_a?(Array)
13
- value.map { |v| BlindIndex.generate_bidx(v, bi) }
12
+ value.map { |v| BlindIndex.generate_bidx(v, **bi) }
14
13
  else
15
- BlindIndex.generate_bidx(value, bi)
14
+ BlindIndex.generate_bidx(value, **bi)
16
15
  end
17
16
  end
18
17
  end
@@ -29,42 +28,19 @@ module BlindIndex
29
28
  end
30
29
  end
31
30
 
32
- # ActiveRecord 4.2
33
- module PredicateBuilder
34
- def resolve_column_aliases(klass, hash)
35
- new_hash = super
36
- if has_blind_indexes?(klass)
37
- hash.each do |key, _|
38
- if key.respond_to?(:to_sym) && (bi = klass.blind_indexes[key.to_sym]) && !new_hash[key].is_a?(ActiveRecord::StatementCache::Substitute)
39
- value = new_hash.delete(key)
40
- new_hash[bi[:bidx_attribute]] =
41
- if value.is_a?(Array)
42
- value.map { |v| BlindIndex.generate_bidx(v, bi) }
43
- else
44
- BlindIndex.generate_bidx(value, bi)
45
- end
46
- end
47
- end
48
- end
49
- new_hash
50
- end
51
-
52
- @@blind_index_cache = {}
53
-
54
- # memoize for performance
55
- def has_blind_indexes?(klass)
56
- if @@blind_index_cache[klass].nil?
57
- @@blind_index_cache[klass] = klass.respond_to?(:blind_indexes)
31
+ module UniquenessValidator
32
+ def validate_each(record, attribute, value)
33
+ klass = record.class
34
+ if klass.respond_to?(:blind_indexes) && (bi = klass.blind_indexes[attribute])
35
+ value = record.read_attribute_for_validation(bi[:bidx_attribute])
58
36
  end
59
- @@blind_index_cache[klass]
37
+ super(record, attribute, value)
60
38
  end
61
- end
62
39
 
63
- module UniquenessValidator
40
+ # change attribute name here instead of validate_each for better error message
64
41
  if ActiveRecord::VERSION::STRING >= "5.2"
65
42
  def build_relation(klass, attribute, value)
66
43
  if klass.respond_to?(:blind_indexes) && (bi = klass.blind_indexes[attribute])
67
- value = BlindIndex.generate_bidx(value, bi)
68
44
  attribute = bi[:bidx_attribute]
69
45
  end
70
46
  super(klass, attribute, value)
@@ -72,7 +48,6 @@ module BlindIndex
72
48
  else
73
49
  def build_relation(klass, table, attribute, value)
74
50
  if klass.respond_to?(:blind_indexes) && (bi = klass.blind_indexes[attribute])
75
- value = BlindIndex.generate_bidx(value, bi)
76
51
  attribute = bi[:bidx_attribute]
77
52
  end
78
53
  super(klass, table, attribute, value)
@@ -11,7 +11,7 @@ module BlindIndex
11
11
  raise ArgumentError, "Missing field for key generation" if bidx_attribute.to_s.empty?
12
12
 
13
13
  c = "\x7E"*32
14
- root_key = hkdf(BlindIndex.decode_key(@master_key), salt: table.to_s, info: "#{c}#{bidx_attribute}", length: 32, hash: "sha384")
14
+ root_key = hkdf(BlindIndex.decode_key(@master_key, name: "Master key"), salt: table.to_s, info: "#{c}#{bidx_attribute}", length: 32, hash: "sha384")
15
15
  hash_hmac("sha256", pack([table, bidx_attribute, bidx_attribute]), root_key)
16
16
  end
17
17
 
@@ -22,7 +22,7 @@ module BlindIndex
22
22
  end
23
23
 
24
24
  def hkdf(ikm, salt:, info:, length:, hash:)
25
- if OpenSSL::KDF.respond_to?(:hkdf)
25
+ if defined?(OpenSSL::KDF.hkdf)
26
26
  return OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: length, hash: hash)
27
27
  end
28
28
 
@@ -61,28 +61,33 @@ module BlindIndex
61
61
  )
62
62
 
63
63
  define_singleton_method class_method_name do |value|
64
- BlindIndex.generate_bidx(value, blind_indexes[name])
65
- end
66
-
67
- define_singleton_method method_name do |value|
68
- ActiveSupport::Deprecation.warn("Use #{class_method_name} instead")
69
- send(class_method_name, value)
64
+ BlindIndex.generate_bidx(value, **blind_indexes[name])
70
65
  end
71
66
 
72
67
  define_method method_name do
73
- self.send("#{bidx_attribute}=", self.class.send(class_method_name, send(attribute)))
68
+ send("#{bidx_attribute}=", self.class.send(class_method_name, send(attribute)))
74
69
  end
75
70
 
76
71
  if callback
77
- if defined?(ActiveRecord) && self < ActiveRecord::Base
78
- # Active Record
79
- # prevent deprecation warnings
80
- before_validation method_name, if: -> { changes.key?(attribute.to_s) }
81
- else
82
- # Mongoid
83
- # Lockbox only supports attribute_changed?
84
- before_validation method_name, if: -> { send("#{attribute}_changed?") }
72
+ activerecord = defined?(ActiveRecord) && self < ActiveRecord::Base
73
+
74
+ # TODO reuse module
75
+ m = Module.new do
76
+ define_method "#{attribute}=" do |value|
77
+ result = super(value)
78
+ send(method_name)
79
+ result
80
+ end
81
+
82
+ unless activerecord
83
+ define_method "reset_#{attribute}!" do
84
+ result = super()
85
+ send(method_name)
86
+ result
87
+ end
88
+ end
85
89
  end
90
+ prepend m
86
91
  end
87
92
 
88
93
  # use include so user can override
@@ -90,14 +95,14 @@ module BlindIndex
90
95
  end
91
96
  end
92
97
  end
93
- end
94
98
 
95
- module InstanceMethods
96
- def read_attribute_for_validation(key)
97
- if (bi = self.class.blind_indexes[key])
98
- send(bi[:attribute])
99
- else
100
- super
99
+ module InstanceMethods
100
+ def read_attribute_for_validation(key)
101
+ if (bi = self.class.blind_indexes[key])
102
+ send(bi[:attribute])
103
+ else
104
+ super
105
+ end
101
106
  end
102
107
  end
103
108
  end
@@ -26,9 +26,9 @@ module BlindIndex
26
26
 
27
27
  criterion[bidx_key] =
28
28
  if value.is_a?(Array)
29
- value.map { |v| BlindIndex.generate_bidx(v, bi) }
29
+ value.map { |v| BlindIndex.generate_bidx(v, **bi) }
30
30
  else
31
- BlindIndex.generate_bidx(value, bi)
31
+ BlindIndex.generate_bidx(value, **bi)
32
32
  end
33
33
  end
34
34
  end
@@ -39,9 +39,18 @@ module BlindIndex
39
39
  end
40
40
 
41
41
  module UniquenessValidator
42
+ def validate_each(record, attribute, value)
43
+ klass = record.class
44
+ if klass.respond_to?(:blind_indexes) && (bi = klass.blind_indexes[attribute])
45
+ value = record.read_attribute_for_validation(bi[:bidx_attribute])
46
+ end
47
+ super(record, attribute, value)
48
+ end
49
+
50
+ # change attribute name here instead of validate_each for better error message
42
51
  def create_criteria(base, document, attribute, value)
43
- if base.respond_to?(:blind_indexes) && (bi = base.blind_indexes[attribute])
44
- value = BlindIndex.generate_bidx(value, bi)
52
+ klass = document.class
53
+ if klass.respond_to?(:blind_indexes) && (bi = klass.blind_indexes[attribute])
45
54
  attribute = bi[:bidx_attribute]
46
55
  end
47
56
  super(base, document, attribute, value)
@@ -1,3 +1,3 @@
1
1
  module BlindIndex
2
- VERSION = "1.0.1"
2
+ VERSION = "2.1.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: 1.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-16 00:00:00.000000000 Z
11
+ date: 2020-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -25,19 +25,19 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5'
27
27
  - !ruby/object:Gem::Dependency
28
- name: argon2
28
+ name: argon2-kdf
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '2'
33
+ version: 0.1.1
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '2'
40
+ version: 0.1.1
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -174,6 +174,7 @@ files:
174
174
  - LICENSE.txt
175
175
  - README.md
176
176
  - lib/blind_index.rb
177
+ - lib/blind_index/backfill.rb
177
178
  - lib/blind_index/extensions.rb
178
179
  - lib/blind_index/key_generator.rb
179
180
  - lib/blind_index/model.rb
@@ -198,7 +199,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
198
199
  - !ruby/object:Gem::Version
199
200
  version: '0'
200
201
  requirements: []
201
- rubygems_version: 3.0.4
202
+ rubygems_version: 3.1.2
202
203
  signing_key:
203
204
  specification_version: 4
204
205
  summary: Securely search encrypted database fields