blind_index 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e26dcb60e11724a65676d7f36d31cbcf7f261fb8
4
+ data.tar.gz: a1308e6c01819a59ef5e3c041b77cab2a93995c5
5
+ SHA512:
6
+ metadata.gz: bfa731e6ad5e26ed636e13da6f2ec4a2c292e121c35a6a6abde027faaab80964931f035a6cdd4599df71d243884d883784bf66a8ed9a7cd770bb7cccfdb7beb7
7
+ data.tar.gz: 1a2e24f6e6fd38b1406c5b6585953b99de642850b5707ddd052dbf1732b14043c3ad962d9e874a8d2dbdc4a40e250e3634699be6e2d0e4608cfc59bac8930f41
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.lock
data/.travis.yml ADDED
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ rvm: 2.4.2
3
+ gemfile:
4
+ - Gemfile
5
+ - test/gemfiles/activerecord50.gemfile
6
+ sudo: false
7
+ before_install: gem install bundler
8
+ script: bundle exec rake test
9
+ notifications:
10
+ email:
11
+ on_success: never
12
+ on_failure: change
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.1.0
2
+
3
+ - First release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in blind_index.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2017 Andrew Kane
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # Blind Index
2
+
3
+ Securely query encrypted database fields
4
+
5
+ Designed for use with [attr_encrypted](https://github.com/attr-encrypted/attr_encrypted)
6
+
7
+ ## How It Works
8
+
9
+ We use [this approach](https://www.sitepoint.com/how-to-search-on-securely-encrypted-database-fields/) described 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.
10
+
11
+ ## Getting Started
12
+
13
+ Add these lines to your application’s Gemfile:
14
+
15
+ ```ruby
16
+ gem 'attr_encrypted'
17
+ gem 'blind_index'
18
+ ```
19
+
20
+ Add columns for the encrypted data and the blind index
21
+
22
+ ```ruby
23
+ # encrypted data
24
+ add_column :users, :encrypted_email, :text
25
+ add_column :users, :encrypted_email_iv, :text
26
+
27
+ # blind index
28
+ add_column :users, :encrypted_email_bidx, :text
29
+ add_index :users, :encrypted_email_bidx
30
+ ```
31
+
32
+ Generate one key for encryption and one key for hashing and set them in your environment ([dotenv](https://github.com/bkeepers/dotenv) is great for this). For development, you can use these:
33
+
34
+ ```sh
35
+ EMAIL_ENCRYPTION_KEY=00000000000000000000000000000000
36
+ EMAIL_BLIND_INDEX_KEY=99999999999999999999999999999999
37
+ ```
38
+
39
+ And add to your model
40
+
41
+ ```ruby
42
+ class User < ApplicationRecord
43
+ attr_encrypted :email, key: ENV["EMAIL_ENCRYPTION_KEY"]
44
+
45
+ blind_index :email, key: ENV["EMAIL_BLIND_INDEX_KEY"]
46
+ end
47
+ ```
48
+
49
+ And query away
50
+
51
+ ```ruby
52
+ User.where(email: "test@example.org")
53
+ ```
54
+
55
+ ## Validations
56
+
57
+ To prevent duplicates, use:
58
+
59
+ ```ruby
60
+ class User < ApplicationRecord
61
+ validates :email, uniqueness: true
62
+ end
63
+ ```
64
+
65
+ We also recommend adding a unique index to the blind index column through a database migration.
66
+
67
+ ## Expressions
68
+
69
+ You can apply expressions to attributes before indexing and searching. This gives you the the ability to perform case-insensitive searches and more.
70
+
71
+ ```ruby
72
+ class User < ApplicationRecord
73
+ blind_index :email, expression: -> (v) { v.downcase } ...
74
+ end
75
+ ```
76
+
77
+ ## Multiple Indexes
78
+
79
+ You may want multiple blind indexes for an attribute. To do this, add another column:
80
+
81
+ ```ruby
82
+ add_column :users, :encrypted_email_ci_bidx, :text
83
+ add_index :users, :encrypted_email_ci_bidx
84
+ ```
85
+
86
+ And update your model
87
+
88
+ ```ruby
89
+ class User < ApplicationRecord
90
+ blind_index :email, ...
91
+ blind_index :email_ci, attribute: :email, expression: -> (v) { v.downcase } ...
92
+ end
93
+ ```
94
+
95
+ Search with:
96
+
97
+ ```ruby
98
+ User.where(email_ci: "test@example.org")
99
+ ```
100
+
101
+ ## Key Stretching
102
+
103
+ 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:
104
+
105
+ ```ruby
106
+ class User < ApplicationRecord
107
+ blind_index :email, iterations: 1000000, ...
108
+ end
109
+ ```
110
+
111
+ The default is `10000`. Changing this value requires you to recompute the blind index.
112
+
113
+ ## History
114
+
115
+ View the [changelog](https://github.com/ankane/blind_index/blob/master/CHANGELOG.md)
116
+
117
+ ## Contributing
118
+
119
+ Everyone is encouraged to help improve this project. Here are a few ways you can help:
120
+
121
+ - [Report bugs](https://github.com/ankane/blind_index/issues)
122
+ - Fix bugs and [submit pull requests](https://github.com/ankane/blind_index/pulls)
123
+ - Write, clarify, or fix documentation
124
+ - Suggest or add new features
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ t.warning = false
9
+ end
10
+
11
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "blind_index"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,31 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "blind_index/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "blind_index"
8
+ spec.version = BlindIndex::VERSION
9
+ spec.authors = ["Andrew Kane"]
10
+ spec.email = ["andrew@chartkick.com"]
11
+
12
+ spec.summary = "Securely query encrypted database fields"
13
+ spec.homepage = "https://github.com/ankane/blind_index"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "activesupport", ">= 5"
24
+
25
+ spec.add_development_dependency "bundler"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "minitest"
28
+ spec.add_development_dependency "attr_encrypted"
29
+ spec.add_development_dependency "activerecord"
30
+ spec.add_development_dependency "sqlite3"
31
+ end
@@ -0,0 +1,35 @@
1
+ module BlindIndex
2
+ module Extensions
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 (bi = klass.blind_indexes[key])
9
+ new_hash[bi[:bidx_attribute]] = BlindIndex.generate_bidx(new_hash.delete(key), bi)
10
+ end
11
+ end
12
+ end
13
+ new_hash
14
+ end
15
+
16
+ # memoize for performance
17
+ def has_blind_indexes?
18
+ unless defined?(@has_blind_indexes)
19
+ @has_blind_indexes = klass.respond_to?(:blind_indexes)
20
+ end
21
+ @has_blind_indexes
22
+ end
23
+ end
24
+
25
+ module UniquenessValidator
26
+ def build_relation(klass, table, attribute, value)
27
+ if klass.respond_to?(:blind_indexes) && (bi = klass.blind_indexes[attribute])
28
+ value = BlindIndex.generate_bidx(value, bi)
29
+ attribute = bi[:bidx_attribute]
30
+ end
31
+ super(klass, table, attribute, value)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ module BlindIndex
2
+ module Model
3
+ def blind_index(name, key: nil, iterations: nil, attribute: nil, expression: nil, bidx_attribute: nil)
4
+ iterations ||= 10000
5
+ attribute ||= name
6
+ bidx_attribute ||= :"encrypted_#{name}_bidx"
7
+
8
+ name = name.to_sym
9
+ attribute = attribute.to_sym
10
+ method_name = :"compute_#{name}_bidx"
11
+
12
+ class_eval do
13
+ class << self
14
+ def blind_indexes
15
+ @blind_indexes ||= {}
16
+ end unless respond_to?(:blind_indexes)
17
+ end
18
+
19
+ raise BlindIndex::Error, "Duplicate blind index: #{name}" if blind_indexes[name]
20
+
21
+ blind_indexes[name] = {
22
+ key: key,
23
+ iterations: iterations,
24
+ attribute: attribute,
25
+ expression: expression,
26
+ bidx_attribute: bidx_attribute
27
+ }
28
+
29
+ before_validation method_name, if: -> { changes.key?(attribute.to_s) }
30
+ define_method method_name do
31
+ self.send("#{bidx_attribute}=", BlindIndex.generate_bidx(send(attribute), self.class.blind_indexes[name]))
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,3 @@
1
+ module BlindIndex
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,33 @@
1
+ # dependencies
2
+ require "active_support"
3
+
4
+ # modules
5
+ require "blind_index/extensions"
6
+ require "blind_index/model"
7
+ require "blind_index/version"
8
+
9
+ module BlindIndex
10
+ class Error; end
11
+
12
+ def self.generate_bidx(value, key:, iterations:, expression: nil, **options)
13
+ raise BlindIndex::Error, "Missing key for blind index" unless key
14
+
15
+ # apply expression
16
+ value = expression.call(value) if expression
17
+
18
+ # generate hash
19
+ digest = OpenSSL::Digest::SHA256.new
20
+ value = OpenSSL::PKCS5.pbkdf2_hmac(value.to_s, key, iterations, digest.digest_length, digest)
21
+
22
+ # encode
23
+ [value].pack("m")
24
+ end
25
+ end
26
+
27
+ ActiveSupport.on_load(:active_record) do
28
+ extend BlindIndex::Model
29
+ ActiveRecord::TableMetadata.prepend(BlindIndex::Extensions::TableMetadata)
30
+ if ActiveRecord::VERSION::STRING.start_with?("5.0.")
31
+ ActiveRecord::Validations::UniquenessValidator.prepend(BlindIndex::Extensions::UniquenessValidator)
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: blind_index
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Kane
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-12-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
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: minitest
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: attr_encrypted
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: activerecord
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: sqlite3
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
+ description:
112
+ email:
113
+ - andrew@chartkick.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".travis.yml"
120
+ - CHANGELOG.md
121
+ - Gemfile
122
+ - LICENSE.txt
123
+ - README.md
124
+ - Rakefile
125
+ - bin/console
126
+ - bin/setup
127
+ - blind_index.gemspec
128
+ - lib/blind_index.rb
129
+ - lib/blind_index/extensions.rb
130
+ - lib/blind_index/model.rb
131
+ - lib/blind_index/version.rb
132
+ homepage: https://github.com/ankane/blind_index
133
+ licenses:
134
+ - MIT
135
+ metadata: {}
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 2.6.13
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: Securely query encrypted database fields
156
+ test_files: []