attribute_queryable_encrypted 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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