blind_index 2.0.2 → 2.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: 06112a25e062c04740b0a01173300d37c364c05ef3bef0de0fa214e3dfbca61e
4
- data.tar.gz: 41c534b7fd3266559821bba50d122a08adbf3215212304dc7c911cb62e204596
3
+ metadata.gz: 89f2640c25eea46ce76f32e3f8c725744f356c1037bc719c7cf2e1ab2728a1ee
4
+ data.tar.gz: 984cf9b1d682d5662129e0ce9dee0047c3c57fbbc6d2922913bc59c0024334dc
5
5
  SHA512:
6
- metadata.gz: 7f3369a1b5ba9b36be21e18c3a41ea9060c779f7cb2b92c2b2539144acd5fb01d99a797b123f6f7b1a97dd53b92c6ff45ba4c5517e43bf99f3e8363875ade522
7
- data.tar.gz: 0e5a857dd66532f72635c5ec13f56c4e864f8197533a3d300a5630cda8e9dc2d62b3c3bf771c794df2af668a2249830f6d0ba4e06a396f303131465f17dbd6a2
6
+ metadata.gz: 99d6f55c8664ba560ac4f7171a402eb6a18347d7be1bfb70425b8f506ba829c808a1e18fc26fda5d8eb7bb88ec7cdcce989e0aadd7e94c22cbfefc915f01150d
7
+ data.tar.gz: b9df49cf70f1e62e2bc753c0dc065c5fb236b38962cc3bdd7d15d6a7068521aaacd051ac996a9fc05018c48a32b4ca84494f268abee9900b447d3fb107416063
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## 2.3.0 (2022-01-16)
2
+
3
+ - Added blind indexes to `filter_attributes`
4
+ - Dropped support for Ruby < 2.6 and Rails < 5.2
5
+
6
+ ## 2.2.0 (2020-09-07)
7
+
8
+ - Added support for `where` with table in Active Record 5.2+
9
+
10
+ ## 2.1.1 (2020-08-14)
11
+
12
+ - Fixed `version` option
13
+
14
+ ## 2.1.0 (2020-07-06)
15
+
16
+ - Improved performance of uniqueness validations
17
+ - Fixed deprecation warnings in Ruby 2.7 with Mongoid
18
+
1
19
  ## 2.0.2 (2020-06-01)
2
20
 
3
21
  - Improved error message for bad key length
@@ -7,7 +25,7 @@
7
25
 
8
26
  - Added `BlindIndex.backfill` method
9
27
 
10
- ## 2.0.0 (2019-02-10)
28
+ ## 2.0.0 (2020-02-10)
11
29
 
12
30
  - Blind indexes are updated immediately instead of in a `before_validation` callback
13
31
  - Better Lockbox integration - no need to generate a separate key
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2017-2020 Andrew Kane
1
+ Copyright (c) 2017-2021 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
@@ -6,11 +6,11 @@ Works with [Lockbox](https://github.com/ankane/lockbox) ([full example](https://
6
6
 
7
7
  Learn more about [securing sensitive data in Rails](https://ankane.org/sensitive-data-rails)
8
8
 
9
- [![Build Status](https://travis-ci.org/ankane/blind_index.svg?branch=master)](https://travis-ci.org/ankane/blind_index)
9
+ [![Build Status](https://github.com/ankane/blind_index/workflows/build/badge.svg?branch=master)](https://github.com/ankane/blind_index/actions)
10
10
 
11
11
  ## How It Works
12
12
 
13
- We use [this approach](https://paragonie.com/blog/2017/05/building-searchable-encrypted-databases-with-php-and-sql) 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 to the value we’re searching and then perform a database search. This results in performant queries for exact matches. `LIKE` queries are not possible, but you can index expressions.
13
+ We use [this approach](https://paragonie.com/blog/2017/05/building-searchable-encrypted-databases-with-php-and-sql) 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 to the value we’re searching and then perform a database search. This results in performant queries for exact matches. Efficient `LIKE` queries are [not possible](#like-ilike-and-full-text-searching), but you can index expressions.
14
14
 
15
15
  ## Leakage
16
16
 
@@ -26,13 +26,13 @@ Add this line to your application’s Gemfile:
26
26
  gem 'blind_index'
27
27
  ```
28
28
 
29
- ## Getting Started
29
+ ## Prep
30
30
 
31
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.
32
32
 
33
33
  Also, if you use attr_encrypted, [generate a key](#key-generation).
34
34
 
35
- ---
35
+ ## Getting Started
36
36
 
37
37
  Create a migration to add a column for the blind index
38
38
 
@@ -69,9 +69,19 @@ And query away
69
69
  User.where(email: "test@example.org")
70
70
  ```
71
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
+
72
82
  ## Validations
73
83
 
74
- To prevent duplicates, use:
84
+ You can use blind indexes for uniqueness validations.
75
85
 
76
86
  ```ruby
77
87
  class User < ApplicationRecord
@@ -79,15 +89,27 @@ class User < ApplicationRecord
79
89
  end
80
90
  ```
81
91
 
82
- 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.
83
93
 
84
- ## Expressions
94
+ ```ruby
95
+ add_index :users, :email_bidx, unique: true
96
+ ```
85
97
 
86
- 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:
87
108
 
88
109
  ```ruby
89
110
  class User < ApplicationRecord
90
111
  blind_index :email, expression: ->(v) { v.downcase }
112
+ validates :email, uniqueness: true # for best performance, leave out {case_sensitive: false}
91
113
  end
92
114
  ```
93
115
 
@@ -240,6 +262,7 @@ For Mongoid, use:
240
262
  ```ruby
241
263
  class User
242
264
  field :email_bidx, type: String
265
+ index({email_bidx: 1})
243
266
  end
244
267
  ```
245
268
 
@@ -267,6 +290,30 @@ or create `config/initializers/blind_index.rb` with something like
267
290
  BlindIndex.master_key = Rails.application.credentials.blind_index_master_key
268
291
  ```
269
292
 
293
+ ## LIKE, ILIKE, and Full-Text Searching
294
+
295
+ Unfortunately, blind indexes can’t be used for `LIKE`, `ILIKE`, or full-text searching. Instead, records must be loaded, decrypted, and searched in memory.
296
+
297
+ For `LIKE`, use:
298
+
299
+ ```ruby
300
+ User.select { |u| u.email.include?("value") }
301
+ ```
302
+
303
+ For `ILIKE`, use:
304
+
305
+ ```ruby
306
+ User.select { |u| u.email =~ /value/i }
307
+ ```
308
+
309
+ For full-text or fuzzy searching, use a gem like [FuzzyMatch](https://github.com/seamusabshere/fuzzy_match):
310
+
311
+ ```ruby
312
+ FuzzyMatch.new(User.all, read: :email).find("value")
313
+ ```
314
+
315
+ If the number of records is large, try to find a way to narrow it down. An [expression index](#expressions) is one way to do this, but leaks which records have the same value of the expression, so use it carefully.
316
+
270
317
  ## Reference
271
318
 
272
319
  Set default options in an initializer with:
@@ -436,3 +483,5 @@ cd blind_index
436
483
  bundle install
437
484
  bundle exec rake test
438
485
  ```
486
+
487
+ For security issues, send an email to the address on [this page](https://github.com/ankane).
@@ -1,24 +1,6 @@
1
1
  module BlindIndex
2
2
  module Extensions
3
3
  module TableMetadata
4
- def resolve_column_aliases(hash)
5
- new_hash = super
6
- if has_blind_indexes?
7
- hash.each do |key, _|
8
- if key.respond_to?(:to_sym) && (bi = klass.blind_indexes[key.to_sym]) && !new_hash[key].is_a?(ActiveRecord::StatementCache::Substitute)
9
- value = new_hash.delete(key)
10
- new_hash[bi[:bidx_attribute]] =
11
- if value.is_a?(Array)
12
- value.map { |v| BlindIndex.generate_bidx(v, **bi) }
13
- else
14
- BlindIndex.generate_bidx(value, **bi)
15
- end
16
- end
17
- end
18
- end
19
- new_hash
20
- end
21
-
22
4
  # memoize for performance
23
5
  def has_blind_indexes?
24
6
  unless defined?(@has_blind_indexes)
@@ -28,23 +10,38 @@ module BlindIndex
28
10
  end
29
11
  end
30
12
 
13
+ module PredicateBuilder
14
+ # https://github.com/rails/rails/commit/56f30962b84fc53b76001301fb830c1594fd377e
15
+ def build(attribute, value, *args)
16
+ if table.has_blind_indexes? && (bi = table.send(:klass).blind_indexes[attribute.name.to_sym]) && !value.is_a?(ActiveRecord::StatementCache::Substitute)
17
+ attribute = attribute.relation[bi[:bidx_attribute]]
18
+ value =
19
+ if value.is_a?(Array)
20
+ value.map { |v| BlindIndex.generate_bidx(v, **bi) }
21
+ else
22
+ BlindIndex.generate_bidx(value, **bi)
23
+ end
24
+ end
25
+
26
+ super(attribute, value, *args)
27
+ end
28
+ end
29
+
31
30
  module UniquenessValidator
32
- if ActiveRecord::VERSION::STRING >= "5.2"
33
- def build_relation(klass, attribute, value)
34
- if klass.respond_to?(:blind_indexes) && (bi = klass.blind_indexes[attribute])
35
- value = BlindIndex.generate_bidx(value, **bi)
36
- attribute = bi[:bidx_attribute]
37
- end
38
- super(klass, attribute, value)
31
+ def validate_each(record, attribute, value)
32
+ klass = record.class
33
+ if klass.respond_to?(:blind_indexes) && (bi = klass.blind_indexes[attribute])
34
+ value = record.read_attribute_for_validation(bi[:bidx_attribute])
39
35
  end
40
- else
41
- def build_relation(klass, table, attribute, value)
42
- if klass.respond_to?(:blind_indexes) && (bi = klass.blind_indexes[attribute])
43
- value = BlindIndex.generate_bidx(value, **bi)
44
- attribute = bi[:bidx_attribute]
45
- end
46
- super(klass, table, attribute, value)
36
+ super(record, attribute, value)
37
+ end
38
+
39
+ # change attribute name here instead of validate_each for better error message
40
+ def build_relation(klass, attribute, value)
41
+ if klass.respond_to?(:blind_indexes) && (bi = klass.blind_indexes[attribute])
42
+ attribute = bi[:bidx_attribute]
47
43
  end
44
+ super(klass, attribute, value)
48
45
  end
49
46
  end
50
47
 
@@ -10,7 +10,7 @@ module BlindIndex
10
10
  # check here so we validate rotate options as well
11
11
  unknown_keywords = options.keys - [:algorithm, :attribute, :bidx_attribute,
12
12
  :callback, :cost, :encode, :expression, :insecure_key, :iterations, :key,
13
- :legacy, :master_key, :size, :slow]
13
+ :legacy, :master_key, :size, :slow, :version]
14
14
  raise ArgumentError, "unknown keywords: #{unknown_keywords.join(", ")}" if unknown_keywords.any?
15
15
 
16
16
  attribute = options[:attribute] || name
@@ -36,6 +36,14 @@ module BlindIndex
36
36
  key ||= -> { BlindIndex.index_key(table: try(:table_name) || collection_name.to_s, bidx_attribute: bidx_attribute, master_key: options[:master_key], encode: false) }
37
37
 
38
38
  class_eval do
39
+ activerecord = defined?(ActiveRecord) && self < ActiveRecord::Base
40
+
41
+ if activerecord && ActiveRecord::VERSION::MAJOR >= 6
42
+ # blind index value isn't really sensitive
43
+ # but don't need to show it in the Rails console
44
+ self.filter_attributes += [/\A#{Regexp.escape(bidx_attribute)}\z/]
45
+ end
46
+
39
47
  @blind_indexes ||= {}
40
48
 
41
49
  unless respond_to?(:blind_indexes)
@@ -69,8 +77,6 @@ module BlindIndex
69
77
  end
70
78
 
71
79
  if callback
72
- activerecord = defined?(ActiveRecord) && self < ActiveRecord::Base
73
-
74
80
  # TODO reuse module
75
81
  m = Module.new do
76
82
  define_method "#{attribute}=" do |value|
@@ -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 = "2.0.2"
2
+ VERSION = "2.3.0"
3
3
  end
data/lib/blind_index.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  # dependencies
2
2
  require "active_support"
3
- require "openssl"
4
3
  require "argon2/kdf"
5
4
 
5
+ # stdlib
6
+ require "openssl"
7
+
6
8
  # modules
7
9
  require "blind_index/backfill"
8
10
  require "blind_index/key_generator"
@@ -140,18 +142,13 @@ ActiveSupport.on_load(:active_record) do
140
142
 
141
143
  ActiveRecord::TableMetadata.prepend(BlindIndex::Extensions::TableMetadata)
142
144
  ActiveRecord::DynamicMatchers::Method.prepend(BlindIndex::Extensions::DynamicMatchers)
143
-
144
- unless ActiveRecord::VERSION::STRING.start_with?("5.1.")
145
- ActiveRecord::Validations::UniquenessValidator.prepend(BlindIndex::Extensions::UniquenessValidator)
146
- end
145
+ ActiveRecord::Validations::UniquenessValidator.prepend(BlindIndex::Extensions::UniquenessValidator)
146
+ ActiveRecord::PredicateBuilder.prepend(BlindIndex::Extensions::PredicateBuilder)
147
147
  end
148
148
 
149
- if defined?(Mongoid)
150
- # TODO find better ActiveModel hook
151
- require "active_model/callbacks"
152
- ActiveModel::Callbacks.include(BlindIndex::Model)
153
-
149
+ ActiveSupport.on_load(:mongoid) do
154
150
  require "blind_index/mongoid"
151
+ Mongoid::Document::ClassMethods.include(BlindIndex::Model)
155
152
  Mongoid::Criteria.prepend(BlindIndex::Mongoid::Criteria)
156
153
  Mongoid::Validatable::UniquenessValidator.prepend(BlindIndex::Mongoid::UniquenessValidator)
157
154
  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: 2.0.2
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-01 00:00:00.000000000 Z
11
+ date: 2022-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5'
19
+ version: '5.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '5'
26
+ version: '5.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: argon2-kdf
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -38,134 +38,8 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.1.1
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: rake
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: minitest
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: attr_encrypted
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: activerecord
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: sqlite3
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: scrypt
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'
153
- - !ruby/object:Gem::Dependency
154
- name: lockbox
155
- requirement: !ruby/object:Gem::Requirement
156
- requirements:
157
- - - ">="
158
- - !ruby/object:Gem::Version
159
- version: '0.2'
160
- type: :development
161
- prerelease: false
162
- version_requirements: !ruby/object:Gem::Requirement
163
- requirements:
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: '0.2'
167
- description:
168
- email: andrew@chartkick.com
41
+ description:
42
+ email: andrew@ankane.org
169
43
  executables: []
170
44
  extensions: []
171
45
  extra_rdoc_files: []
@@ -184,7 +58,7 @@ homepage: https://github.com/ankane/blind_index
184
58
  licenses:
185
59
  - MIT
186
60
  metadata: {}
187
- post_install_message:
61
+ post_install_message:
188
62
  rdoc_options: []
189
63
  require_paths:
190
64
  - lib
@@ -192,15 +66,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
192
66
  requirements:
193
67
  - - ">="
194
68
  - !ruby/object:Gem::Version
195
- version: '2.4'
69
+ version: '2.6'
196
70
  required_rubygems_version: !ruby/object:Gem::Requirement
197
71
  requirements:
198
72
  - - ">="
199
73
  - !ruby/object:Gem::Version
200
74
  version: '0'
201
75
  requirements: []
202
- rubygems_version: 3.1.2
203
- signing_key:
76
+ rubygems_version: 3.3.3
77
+ signing_key:
204
78
  specification_version: 4
205
79
  summary: Securely search encrypted database fields
206
80
  test_files: []