attribute_queryable_encrypted 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ spec/db/*.sqlite3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in attribute_queryable_encrypted.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,29 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ attribute_queryable_encrypted (0.0.1)
5
+ active_support (>= 3.0)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ active_support (3.0.0)
11
+ activesupport (= 3.0.0)
12
+ activesupport (3.0.0)
13
+ diff-lcs (1.1.3)
14
+ rspec (2.7.0)
15
+ rspec-core (~> 2.7.0)
16
+ rspec-expectations (~> 2.7.0)
17
+ rspec-mocks (~> 2.7.0)
18
+ rspec-core (2.7.1)
19
+ rspec-expectations (2.7.0)
20
+ diff-lcs (~> 1.1.2)
21
+ rspec-mocks (2.7.0)
22
+
23
+ PLATFORMS
24
+ ruby
25
+
26
+ DEPENDENCIES
27
+ active_support (>= 3.0)
28
+ attribute_queryable_encrypted!
29
+ rspec
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "attribute_queryable_encrypted/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "attribute_queryable_encrypted"
7
+ s.version = AttributeQueryableEncrypted::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Scott Burton"]
10
+ s.email = ["scott@chaione.com"]
11
+ s.homepage = "https://github.com/chaione/attribute_queryable_encrypted"
12
+ s.summary = %q{Makes querying encrypted & salted attributes mildly less horrible}
13
+ s.description = %q{Makes querying encrypted & salted attributes mildly less horrible}
14
+
15
+ s.rubyforge_project = "attribute_queryable_encrypted"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency "active_support", ">= 3.0"
23
+ s.add_development_dependency "rspec"
24
+ end
@@ -0,0 +1,10 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/array/extract_options'
3
+ require 'active_support/core_ext/class/attribute'
4
+ require 'digest'
5
+ require 'attribute_queryable_encrypted/core_ext/lower_higher'
6
+ require 'attribute_queryable_encrypted/core_ext/prefix'
7
+ require 'attribute_queryable_encrypted/core_ext/stretch_digest'
8
+ require 'attribute_queryable_encrypted/prefix_attributes'
9
+ require 'attribute_queryable_encrypted/railtie' if defined? Rails
10
+ require 'attribute_queryable_encrypted/adapters/active_record' if defined? ActiveRecord
@@ -0,0 +1,46 @@
1
+ module AttributeQueryableEncrypted
2
+ module Adapters
3
+ module ActiveRecord
4
+ extend ActiveSupport::Concern
5
+
6
+ include AttributeQueryableEncrypted::PrefixAttributes
7
+
8
+ included do
9
+ attrbute_queryable_encrypted_default_options[:encode] = true
10
+ end
11
+
12
+ module ClassMethods
13
+ def attribute_queryable_encrypted *args
14
+ define_attribute_methods rescue nil
15
+ super *args
16
+
17
+ args.reject { |arg| arg.is_a?(Hash) }.each do |attribute|
18
+ options = queryable_encrypted_attributes[attribute]
19
+
20
+ find_all_by_method = proc do |prefix_value|
21
+ send("find_all_by_#{[options[:prefix], attribute, options[:suffix]].join('_')}", prefix_encrypt(prefix_value, options))
22
+ end
23
+
24
+ find_by_method = proc do |original_value|
25
+ send("find_all_by_#{[options[:prefix], attribute].join('_')}", original_value.prefix(options[:length])).detect do |result|
26
+ result[attribute] === original_value
27
+ end
28
+ end
29
+
30
+ singleton = class << self
31
+ self
32
+ end
33
+
34
+ alias_method "original_find_by_#{attribute}", "find_by_#{attribute}" if respond_to?(attribute)
35
+
36
+ singleton.send(:define_method, "find_all_by_#{[options[:prefix], attribute].join('_')}", find_all_by_method)
37
+ singleton.send(:define_method, "find_by_#{attribute}", find_by_method)
38
+
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ ActiveRecord::Base.send(:include, AttributeQueryableEncrypted::Adapters::ActiveRecord)
@@ -0,0 +1,17 @@
1
+ module AttributeQueryableEncrypted
2
+ module CoreExt
3
+ module LowerHigher
4
+ # Returns the lower of self or n
5
+ def lower(n)
6
+ self < n ? self : n
7
+ end
8
+
9
+ # Returns the higher of self or n
10
+ def higher(n)
11
+ self > n ? self : n
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ Numeric.send(:include, AttributeQueryableEncrypted::CoreExt::LowerHigher)
@@ -0,0 +1,24 @@
1
+ module AttributeQueryableEncrypted
2
+ module CoreExt
3
+ module Prefix
4
+ # Returns an integer of the available length provided
5
+ # a fixed or percentage-based requested length, or self's
6
+ # length, whichever is lowest.
7
+ #
8
+ # Examples
9
+ # "This is a string".prefix_length #=> 8
10
+ # "This is a string".prefix_length(1000) => 16
11
+ # "This is a string".prefix_length("75%") => 12
12
+ #
13
+ def prefix_length(requested_length)
14
+ requested_length.is_a?(Numeric) ? length.lower(requested_length) : (length/(100/requested_length.match(/^([0-9.]+)%$/)[0].to_f)).ceil
15
+ end
16
+
17
+ def prefix(requested_length)
18
+ self[0, prefix_length(requested_length)]
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ String.send(:include, AttributeQueryableEncrypted::CoreExt::Prefix)
@@ -0,0 +1,19 @@
1
+ module AttributeQueryableEncrypted
2
+ module CoreExt
3
+ module StretchDigest
4
+ def stretch_digest(options={})
5
+ options[:digest] ||= Digest::SHA2
6
+ options[:stretches] ||= 1
7
+
8
+ digest = options[:digest].new
9
+ options[:stretches].times do
10
+ string = options[:key] ? self + options[:key] : self
11
+ digest.update(string)
12
+ end
13
+ digest.to_s
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ String.send(:include, AttributeQueryableEncrypted::CoreExt::StretchDigest)
@@ -0,0 +1,81 @@
1
+ module AttributeQueryableEncrypted
2
+ module PrefixAttributes
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :queryable_encrypted_attributes, :attrbute_queryable_encrypted_default_options
7
+ self.queryable_encrypted_attributes = {}
8
+ self.attrbute_queryable_encrypted_default_options = {
9
+ :length => "50%",
10
+ :prefix => "prefix",
11
+ :suffix => "digest",
12
+ :encode => false,
13
+ :stretches => 3
14
+ }
15
+ end
16
+
17
+ module ClassMethods
18
+
19
+ # Assigns a digest-hashed value to an attribute writer using a portion of the
20
+ # value assigned to each attribute's normal writer. The digest-hashed prefix
21
+ # can then be used to identify other objects with the same prefix without
22
+ # revealing the underlying value.
23
+ #
24
+ # Example:
25
+ # --------
26
+ # class HiddenValue
27
+ # include AttributeQueryableEncrypted::PrefixAttributes
28
+ # attr_writer :data
29
+ # attr_accessor :prefix_data_digest
30
+ # attribute_queryable_encrypted :data
31
+ # end
32
+ #
33
+ # hider = HiddenValue.new
34
+ #
35
+ # hider.data = "This is a string"
36
+ # hider.prefix_data_digest
37
+ # # => "a37010c994067764d86540bf479d93b4d0c3bb3955de7b61f951caf2fd0301b0"
38
+ #
39
+ # This technique is valuable when the queryable encrypted attribute is not
40
+ # persisted, or is persisted in a non-deterministic way (i.e. a salted, encrypted
41
+ # database column)
42
+ #
43
+ #
44
+ # Options:
45
+ # --------
46
+ # :length - an integer value length, or percentage expressed as a string ("72%")
47
+ # :prefix - prefix name for the storage accessor. Default is "prefix"
48
+ # :suffix - suffix name for the storage accessor. Defuault is "suffix"
49
+ # :encode - Base64 encode the digest hash, suitable for database persistence. Default is false.
50
+ #
51
+ def attribute_queryable_encrypted *attributes
52
+
53
+ options = attrbute_queryable_encrypted_default_options.merge(attributes.extract_options!)
54
+
55
+ attributes.each do |attribute|
56
+ queryable_encrypted_attributes[attribute] = options
57
+ class_eval do
58
+ alias_method "unprefixed_#{attribute}=".to_sym, "#{attribute}=".to_sym
59
+
60
+ define_method "#{attribute}=", do |*args, &blk|
61
+ send("#{[options[:prefix], attribute, options[:suffix]].join('_')}=".to_sym, prefix_encrypt(args[0], options))
62
+ send("unprefixed_#{attribute}=".to_sym, *args, &blk)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def prefix_encrypt(value, options)
69
+ prefix_encrypted_value = value.prefix(options[:length]).stretch_digest(options)
70
+ prefix_encrypted_value = [prefix_encrypted_value].pack("m*") if options[:encode]
71
+ prefix_encrypted_value
72
+ end
73
+ end
74
+
75
+ module InstanceMethods
76
+ def prefix_encrypt(value, options)
77
+ self.class.prefix_encrypt(value, options)
78
+ end
79
+ end
80
+ end
81
+ end
File without changes
@@ -0,0 +1,3 @@
1
+ module AttributeQueryableEncrypted
2
+ VERSION = "0.0.1"
3
+ end
data/readme.md ADDED
@@ -0,0 +1,72 @@
1
+ AttributeQueryableEncrypted
2
+ ===========================
3
+ Assigns a digest-hashed value to an attribute writer using a portion of the value assigned to each attribute's normal writer. The digest-hashed prefix can then be used to identify other objects with the same prefix without revealing the underlying value.
4
+
5
+ AttributeQueryableEncrypted was inspried by shuber's excellent [attr_encrypted](https://github.com/shuber/attr_encrypted) gem, and aims for compatibility. It attempts to addresses a shortcoming of encryption, where encrypted columns are queryable when unsalted, but attackable using a precomputed "rainbow table".
6
+
7
+ Selecting multiple candidates with matching prefix digests and subsequently decrypting the full salted/encrypted data field to find a exact match will reduce the need for a full table scan. You should use attr_encrypted, or your own crypto logic, to handle encrypting and decrypting the appropriate full data field.
8
+
9
+ Example:
10
+ --------
11
+ class HiddenValue
12
+ include AttributeQueryableEncrypted::PrefixAttributes
13
+ attr_accessor :prefix_data_digest
14
+ attribute_queryable_encrypted :data
15
+
16
+ def data=(something)
17
+ ...something fancy that obscures the data...
18
+ end
19
+ end
20
+
21
+ hider = HiddenValue.new
22
+
23
+ hider.data = "This is a string"
24
+ hider.prefix_data_digest
25
+ # => "a37010c994067764d86540bf479d93b4d0c3bb3955de7b61f951caf2fd0301b0"
26
+
27
+
28
+ ActiveRecord:
29
+ -------------
30
+ ActiveRecord users gain a query method for their prefix digest column:
31
+
32
+ HiddenValue.find_all_by_prefix_data("This is ")
33
+ # => [#<HiddenValue id: 1, encrypted_data: "MWE2ODg0ZTVmNTA2M2I3MTZmMWQxZGI3NzA0MjgyMzRj...", prefix_data_digest: "MTgyODBlMWFkNGZiMjAyZTc5Y2FiYTcxODZhYTg1OWM3OGNhOWI...">, #<HiddenValue id: 2, encrypted_data: "WQxZGI3NzANTA2M2I3MTZmMj0MjgyMzRMWE2ODg0ZTVm...", prefix_data_digest: "MTgyODBlMWFkNGZiMjAyZTc5Y2FiYTcxODZhYTg1OWM3OGNhOWI...">]
34
+
35
+ The returned records - a subset of the full table - can then be iterated over to find an exact match.
36
+
37
+ A convenience method is provided to do this for you - note that it requires an attribute getter method (but not necessarily a database column) that provides a cleartext match for your query argument:
38
+
39
+ HiddenValue.find_by_prefix_data("This is a string")
40
+ # => #<HiddenValue id: 1, encrypted_data: "MWE2ODg0ZTVmNTA2M2I3MTZmMWQxZGI3NzA0MjgyMzRj...", prefix_data_digest: "MTgyODBlMWFkNGZiMjAyZTc5Y2FiYTcxODZhYTg1OWM3OGNhOWI...">
41
+
42
+ You'll need to create an appropriately-named prefix digest column on your own.
43
+
44
+
45
+ Options:
46
+ --------
47
+ * :length - an integer value length, or percentage expressed as a string ("72%"). Default is "50%".
48
+ * :prefix - prefix name for the storage accessor. Default is "prefix"
49
+ * :suffix - suffix name for the storage accessor. Defuault is "suffix"
50
+ * :encode - Base64 encode the digest hash, suitable for database persistence. Default is false.
51
+ * :stretches - an integer number of iterations through the digest algorithm. More will reduce the ease of a precomputed attack. Default is 3.
52
+ * :key - an optional key to salt the digest algorithm. Default is nil.
53
+
54
+ If you choose to use :stretches and/or :key, you should keep their values secret.
55
+
56
+ Requirements:
57
+ -------------
58
+ * ActiveSupport >= 3.0
59
+ * ActiveRecord >= 3.0 for ActiveRecord usage
60
+
61
+ Warnings
62
+ --------
63
+ * This technique is not without shortcomings, notably that the prefix digest is subject to a precomputed attack.
64
+ * You should consider using secret values for :stretches and :key, and setting the :length option to a level that obscures an appropriate amount of your data without potentially giving away too much.
65
+ * Increasing :stretches incurs a small performance penalty.
66
+ * Decreasing :length can return more records in the initial matched set, potentially decreasing performance.
67
+
68
+ Copyright
69
+ ---------
70
+ (The MIT License)
71
+
72
+ &copy; 2011 (Scott Burton, ChaiOne)
@@ -0,0 +1,31 @@
1
+ require 'active_record'
2
+ require 'logger'
3
+ require 'spec_helper'
4
+
5
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3",
6
+ :database => File.dirname(__FILE__) + "/db/attribute_queryable_encrypted.sqlite3")
7
+
8
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
9
+ ActiveRecord::Base.logger.level = 3
10
+
11
+ load File.dirname(__FILE__) + '/db/schema.rb'
12
+
13
+ class TestModel < ActiveRecord::Base
14
+ attribute_queryable_encrypted :data, :length => 9
15
+ end
16
+
17
+ describe TestModel do
18
+ before(:all) do
19
+ @match1, @match2, @not_match = ["This is a string", "This is another string", "This string doesn't match"].map {|data| TestModel.create(:data => data)}
20
+ end
21
+
22
+ describe "class query methods" do
23
+ it "finds the model instances with the appropriate prefix data" do
24
+ TestModel.find_all_by_prefix_data("This is a").should eql([@match1, @match2])
25
+ end
26
+
27
+ it "finds the first exact match for the full original value" do
28
+ TestModel.find_by_data("This is another string").should eql @match2
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ class TestModel
4
+ attr_accessor :default_length_data, :prefix_default_length_data_digest
5
+ attr_accessor :fixed_length_data, :prefix_fixed_length_data_digest
6
+ attr_accessor :percentage_length_data, :prefix_percentage_length_data_digest
7
+ include AttributeQueryableEncrypted::PrefixAttributes
8
+ attrbute_queryable_encrypted_default_options[:encode] = false
9
+ end
10
+
11
+ describe TestModel do
12
+ let(:input_string) {"It's a wicked string"}
13
+ context "without a :length" do
14
+ before(:all) do
15
+ TestModel.attribute_queryable_encrypted :default_length_data
16
+ end
17
+ before(:each) do
18
+ subject.default_length_data = input_string
19
+ end
20
+
21
+ its(:prefix_default_length_data_digest) {should eql "bc1b6f5cd503fad53c32b002176ca65f0c7409194ecb987825d6875ce1392aa1"}
22
+ its(:default_length_data) {should eql input_string}
23
+ end
24
+
25
+ context "with a :length" do
26
+ context "as an integer" do
27
+ before(:all) do
28
+ TestModel.attribute_queryable_encrypted :fixed_length_data, :length => 14
29
+ end
30
+ before(:each) do
31
+ subject.fixed_length_data = input_string
32
+ end
33
+
34
+ its(:prefix_fixed_length_data_digest) {should eql "9c0dcb0f5d3429f9ac6c8d7bd1e5b056fb326f677806dbd8d813d046e7f3e764"}
35
+ end
36
+
37
+ context "as a string percentage" do
38
+ before(:each) do
39
+ TestModel.attribute_queryable_encrypted :percentage_length_data, :length => "75%"
40
+ subject.percentage_length_data = input_string
41
+ end
42
+
43
+ its(:prefix_percentage_length_data_digest) {should eql "0594e426cced44e4ea358b0dbc10f71ba622661a43a0f3b460a2437e85b43ddc"}
44
+ end
45
+ end
46
+ end
data/spec/db/schema.rb ADDED
@@ -0,0 +1,8 @@
1
+ ActiveRecord::Schema.define do
2
+ self.verbose = false
3
+
4
+ create_table :test_models, :force => true do |t|
5
+ t.string :data
6
+ t.string :prefix_data_digest
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ $: << ".." << "../lib"
2
+ Dir[File.expand_path(File.join(__FILE__, "..", "..", "lib", "attribute_queryable_encrypted.rb"))].each {|file| require file }
3
+ require 'rspec'
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attribute_queryable_encrypted
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Scott Burton
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-11-16 00:00:00 -08:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: active_support
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 3
30
+ - 0
31
+ version: "3.0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rspec
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 0
44
+ version: "0"
45
+ type: :development
46
+ version_requirements: *id002
47
+ description: Makes querying encrypted & salted attributes mildly less horrible
48
+ email:
49
+ - scott@chaione.com
50
+ executables: []
51
+
52
+ extensions: []
53
+
54
+ extra_rdoc_files: []
55
+
56
+ files:
57
+ - .gitignore
58
+ - Gemfile
59
+ - Gemfile.lock
60
+ - Rakefile
61
+ - attribute_queryable_encrypted.gemspec
62
+ - lib/attribute_queryable_encrypted.rb
63
+ - lib/attribute_queryable_encrypted/adapters/active_record.rb
64
+ - lib/attribute_queryable_encrypted/core_ext/lower_higher.rb
65
+ - lib/attribute_queryable_encrypted/core_ext/prefix.rb
66
+ - lib/attribute_queryable_encrypted/core_ext/stretch_digest.rb
67
+ - lib/attribute_queryable_encrypted/prefix_attributes.rb
68
+ - lib/attribute_queryable_encrypted/railtie.rb
69
+ - lib/attribute_queryable_encrypted/version.rb
70
+ - readme.md
71
+ - spec/active_record_adapter_spec.rb
72
+ - spec/attribute_queryable_encrypted_spec.rb
73
+ - spec/db/schema.rb
74
+ - spec/spec_helper.rb
75
+ has_rdoc: true
76
+ homepage: https://github.com/chaione/attribute_queryable_encrypted
77
+ licenses: []
78
+
79
+ post_install_message:
80
+ rdoc_options: []
81
+
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ segments:
98
+ - 0
99
+ version: "0"
100
+ requirements: []
101
+
102
+ rubyforge_project: attribute_queryable_encrypted
103
+ rubygems_version: 1.3.7
104
+ signing_key:
105
+ specification_version: 3
106
+ summary: Makes querying encrypted & salted attributes mildly less horrible
107
+ test_files:
108
+ - spec/active_record_adapter_spec.rb
109
+ - spec/attribute_queryable_encrypted_spec.rb
110
+ - spec/db/schema.rb
111
+ - spec/spec_helper.rb