securely_hashed_attributes 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.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ -f progress
2
+ -c
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in securely_hashed_attributes.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Public Domained!
2
+
3
+ I, Ian D. Eccles, release all claims of copyright on this code and place it
4
+ in the public domain.
5
+
6
+ Do what you like with it, really, go nuts.
7
+
@@ -0,0 +1,116 @@
1
+ ## Overview
2
+
3
+ This gem provides a quick and dirty way to add secure hashing to your
4
+ ActiveRecord models. It relies on the recent refactoring of
5
+ `ActiveRecord::Base::serialize` thus **Rails 3.1+** is **required**.
6
+
7
+ ## Motivation
8
+
9
+ I wrote this gem in response to Aaron Patterson's request for a
10
+ `has_secure_password` implementation that uses the newfound flexibility of
11
+ `ActiveRecord::Base::serialize`. I originally intended to refactor the
12
+ `has_secure_password` method in Rails 3.1, but that poses a problem that
13
+ stems from the fact that `has_secure_password` is defined in ActiveModel::SecurePassword,
14
+ while all of the column serialization jazz is part of `ActiveRecord::Base`.
15
+
16
+ Either `SecurePassword` would need to be moved to `ActiveRecord`, or all of
17
+ the `serialize` jazz would need to be moved to `ActiveModel`. The first
18
+ approach is the easiest, and at the moment only `ActiveRecord::Base` mixes-in
19
+ the `ActiveModel::SecurePassword` module, but it still takes away the option
20
+ of using it in, say, `ActiveResource::Base`. The second approach would be
21
+ a pretty involved undertaking, and while the work done by `serialize` and the
22
+ coders does not depend upon `ActiveRecord`, no actual serialization is
23
+ performed until the model is persisted. As a result, it probably doesn't make
24
+ much sense to refactor `serialize` into `ActiveModel`.
25
+
26
+ In either case, such a refactoring would require some amount of pull amongst
27
+ the Rails community (both components mentioned were put in place by some
28
+ important Rails people) and unlike Jackie Treehorn, I don't pull shit in this
29
+ town. So, I created this gem to illustrate how dead simple the new hotness
30
+ of `serialize` makes this feature. It should work just fine with
31
+ `ActiveRecord`, but it was built as more of a proof of concept than anything
32
+ else.
33
+
34
+ ### Usage
35
+
36
+ Here's an example of how you might use this gem:
37
+
38
+ # Schema: users(:password_hash => String, :bollocks => String,
39
+ # :fancy_pants => String)
40
+ class User < ActiveRecord::Base
41
+ securely_hashes :password, :to => :password_hash
42
+ securely_hashes :bollocks
43
+
44
+ class AlternateCoder
45
+ def self.dump some_value
46
+ # ...
47
+ end
48
+
49
+ def self.load encoded
50
+ # ...
51
+ end
52
+ end
53
+
54
+ securely_hashes :fancy_pants, :with => AlternateCoder
55
+ end
56
+
57
+ some_user = User.new
58
+ some_user.password = 'super secret'
59
+ some_user.bollocks = 'something else to hash'
60
+ some_user.fancy_pants = 'you get the idea'
61
+ some_user.save
62
+ some_user.reload
63
+ some_user.password_hash # => $2a$10blahetcetc...
64
+ some_user.bollocks # => $2a$10yaddayadda...
65
+
66
+ When using the `:to => <column name>` option, the gem will create getters and
67
+ setters for the given attribute name and the setter will pass the value on
68
+ to the actual column. So, in our example of
69
+
70
+ securely_hashes :password, :to => :password_hash
71
+
72
+ the gem defined `password` and `password=` methods on our model automatically.
73
+ This behavior is important to note because it can clobber (or be clobbered) by
74
+ methods explicitly defined on the model. If the `:to => ...` option is not
75
+ used, no methods are created.
76
+
77
+ ### Installing
78
+
79
+ You can use this gem in your rails app by adding
80
+
81
+ gem 'securely_hashed_attributes'
82
+
83
+ to your `Gemfile`. Alternatively, you can install the gem directly:
84
+
85
+ gem install securely_hashed_attributes
86
+
87
+ or you can clone the git repo:
88
+
89
+ git clone git://github.com/iande/securely_hashed_attributes.git
90
+
91
+ I want to re-iterate that this gem was written mostly as a proof of concept,
92
+ and you **must** be running Rails **3.1+** to use it.
93
+
94
+ ### Further Thoughts
95
+
96
+ Hopefully this gem serves its purpose of demonstrating how easy it is to do
97
+ some pretty cool shit with serialized columns in Rails 3.1. You could easily
98
+ add a coder that handles encryption to securely persist data in a column and
99
+ semi-automagically work with the unencrypted data in your app. It also
100
+ provides opportunities for leveraging features of your database of choice, as
101
+ Aaron Patterson demonstrated with his HStore coder at RailsConf 2011.
102
+ Although, you may not want to use `eval` to decode the data.
103
+
104
+
105
+ ### License
106
+
107
+ Public Domained!
108
+
109
+ ### Contributing
110
+
111
+ I'm pretty open to pull requests, if anyone finds utility in this gem and
112
+ wants to contribute additional features. All I ask is that you provide
113
+ adequate documentation and test coverage for any code you contribute. If you
114
+ find the "public domaining" of this code to be problematic, fork the code
115
+ and put it under whatever license you like. You're free to do with this code
116
+ pretty much whatever you like.
@@ -0,0 +1,9 @@
1
+ require 'rake'
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc "Run RSpec"
5
+ RSpec::Core::RakeTask.new do |t|
6
+ t.verbose = false
7
+ end
8
+
9
+ task :default => :spec
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'securely_hashed_attributes'
@@ -0,0 +1,72 @@
1
+ # Kind of necessary for, you know, everything else we do.
2
+ require 'bcrypt'
3
+ require 'active_record'
4
+
5
+ # The primary namespace of this gem
6
+ module SecurelyHashedAttributes
7
+ # The default settings constant. Once the library is fully loaded,
8
+ # +:with+ will still be +nil+, but +:to+ will be set to
9
+ # {SecurelyHashedAttributes::Coders::BCryptPasswordColumn}
10
+ DEFAULT_ATTRIBUTE_OPTIONS = {
11
+ :to => nil,
12
+ :with => nil
13
+ }
14
+ end
15
+ require 'securely_hashed_attributes/version'
16
+ require 'securely_hashed_attributes/coders'
17
+
18
+ # Adds the singleton method
19
+ # {ActiveRecord::Base.securely_hashes securely_hashes} to +ActiveRecord::Base+
20
+ class ActiveRecord::Base
21
+ class << self
22
+ # Adds a secure hash serialization to a column with the help of +bcrypt+.
23
+ # By default, +attrib+ will serve as both the attribute to serialize and
24
+ # the name of the column that will hold the serialized value. If you want
25
+ # to serialize the value to a different column, use the +:to+ option.
26
+ # Hashing of the value assigned to +attrib+ will not occur until the model
27
+ # is persisted.
28
+ #
29
+ # @param [#to_sym] attrib attribute to serialize
30
+ # @param [Hash] opts additional options
31
+ # @option opts [#to_sym] :to The column to serialize +attrib+ to. If this
32
+ # option is specified, a reader attribute will be created for +attrib+
33
+ # and a +<attrib>=+ method will be create that copies the assigned value
34
+ # to the specified column.
35
+ # @option opts [#to_sym] :with (BCryptPasswordColumn)
36
+ # The coder to use to serialize and deserialize values assigned to +attrib+.
37
+ # @see SecurelyHashedAttributes::Coders::BCryptPasswordColumn
38
+ # @example
39
+ # class User < ActiveRecord::Base
40
+ # securely_hashes :password, :to => :password_hash
41
+ # securely_hashes :security_answer
42
+ # end
43
+ #
44
+ # new_user = User.new
45
+ # new_user.password = 'my password'
46
+ # new_user.security_answer = 'I am a turtle'
47
+ # new_user.save
48
+ # new_user.reload
49
+ # new_user.password_hash # => '$2a$10blahetcetc...'
50
+ # new_user.security_answer # => '$2a$10yaddayadda...'
51
+ # new_user.password_hash == 'not my password' # => false
52
+ # new_user.password_hash == 'my password' # => true
53
+ # new_user.security_answer == 'I am a duck' # => false
54
+ # new_user.security_answer == 'I am a turtle' # => true
55
+ def securely_hashes attrib, opts={}
56
+ opts = SecurelyHashedAttributes::DEFAULT_ATTRIBUTE_OPTIONS.merge opts
57
+ attrib = attrib.to_sym
58
+ if opts[:to]
59
+ col = opts[:to].to_sym
60
+ serialize col, opts[:with]
61
+
62
+ attr_reader attrib
63
+ define_method :"#{attrib}=" do |val|
64
+ instance_variable_set(:"@#{attrib}", val)
65
+ self[col] = val
66
+ end
67
+ else
68
+ serialize attrib, opts[:with]
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,5 @@
1
+ # The namespace for the serialize coders we'll provide.
2
+ module SecurelyHashedAttributes::Coders
3
+ end
4
+
5
+ require 'securely_hashed_attributes/coders/bcrypt_password_column'
@@ -0,0 +1,30 @@
1
+ # This coder serializes strings to +BCrypt::Password+ instances which, as the
2
+ # name suggests, are suitable handlers for passwords.
3
+ class SecurelyHashedAttributes::Coders::BCryptPasswordColumn
4
+ # A list of errors to handle when loading
5
+ RESCUE_ERRORS = [ArgumentError, BCrypt::Errors::InvalidHash]
6
+
7
+ # Wrap the given data in the warm blanket of a +BCrypt::Password+
8
+ # @param [#to_s] str data to serialize
9
+ # @return [BCrypt::Password]
10
+ def self.dump str
11
+ BCrypt::Password.create str
12
+ end
13
+
14
+ # Takes a String digest generated by +bcrypt+ and wraps it in a new
15
+ # +BCrypt::Password+ for functionality beyond that of a simple +String+.
16
+ # If +digest+ is +nil+, an empty string is returned. If +bcrypt+ does not
17
+ # recognize +digest+ as a valid hash, +digest+ itself is returned.
18
+ # @param [String] digest a string digest generated by +bcrypt+
19
+ # @return [BCrypt::Password, String]
20
+ def self.load digest
21
+ return '' if digest.nil?
22
+ begin
23
+ BCrypt::Password.new digest
24
+ rescue *RESCUE_ERRORS
25
+ digest
26
+ end
27
+ end
28
+
29
+ SecurelyHashedAttributes::DEFAULT_ATTRIBUTE_OPTIONS[:with] = self
30
+ end
@@ -0,0 +1,4 @@
1
+ module SecurelyHashedAttributes
2
+ # Current version of the securely_hashed_attributes gem
3
+ VERSION = "0.1.0"
4
+ end
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "securely_hashed_attributes/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "securely_hashed_attributes"
7
+ s.version = SecurelyHashedAttributes::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Ian D. Eccles"]
10
+ s.email = ["ian.eccles@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{Add securely hashed attributes to your models}
13
+ s.description = %q{Add securely hashed attributes to your models, serialized with BCrypt}
14
+
15
+ s.rubyforge_project = "securely_hashed_attributes"
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
+ s.add_development_dependency 'rails', '~> 3.1.0.beta1'
22
+ s.add_development_dependency 'rspec', '~> 2.6.0'
23
+ s.add_development_dependency 'with_model', '~> 0.1.5'
24
+ s.add_development_dependency 'sqlite3', '~> 1.3.3'
25
+ s.add_development_dependency 'simplecov', '~> 0.4.2'
26
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ describe SecurelyHashedAttributes::Coders::BCryptPasswordColumn do
4
+ it "should dump a string as a bcrypt password hash" do
5
+ hashed = SecurelyHashedAttributes::Coders::BCryptPasswordColumn.dump('my stringzy')
6
+ hashed.should be_a_kind_of(BCrypt::Password)
7
+ end
8
+
9
+ it "should dump a non string to bcrypt by converting it to a string" do
10
+ hashed = SecurelyHashedAttributes::Coders::BCryptPasswordColumn.dump([1, 2, 3])
11
+ hashed.should be_a_kind_of(BCrypt::Password)
12
+ end
13
+
14
+ it "should load a bcrypt hash as a string" do
15
+ hashed = SecurelyHashedAttributes::Coders::BCryptPasswordColumn.dump('my stringzy')
16
+ loaded = SecurelyHashedAttributes::Coders::BCryptPasswordColumn.load(hashed)
17
+ loaded.should be_a_kind_of(BCrypt::Password)
18
+ loaded.should == 'my stringzy'
19
+ end
20
+
21
+ it "should load a nil as an empty string" do
22
+ loaded = SecurelyHashedAttributes::Coders::BCryptPasswordColumn.load(nil)
23
+ loaded.should_not be_a_kind_of(BCrypt::Password)
24
+ loaded.should == ''
25
+ end
26
+
27
+ it "should load a non bcrypt hash as a plain string" do
28
+ loaded = SecurelyHashedAttributes::Coders::BCryptPasswordColumn.load('sweet lameness')
29
+ loaded.should_not be_a_kind_of(BCrypt::Password)
30
+ loaded.should == 'sweet lameness'
31
+ end
32
+ end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ require 'active_record/connection_adapters/sqlite3_adapter'
4
+
5
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
6
+
7
+ describe ActiveRecord::Base do
8
+ class AlternateCoder
9
+ def self.dump(*_); 'encoded'; end
10
+ def self.load(*_); 'decoded'; end
11
+ end
12
+
13
+ let(:new_model) {
14
+ HashingModel.new
15
+ }
16
+
17
+ with_model :hashing_model do
18
+ table do |t|
19
+ t.string :blathering
20
+ t.string :password_hash
21
+ t.string :chicken_fist_digest
22
+ t.string :big_mclargehuge
23
+ end
24
+
25
+ model do
26
+ attr_reader :stately_dutch, :portly
27
+ alias :stately_dutch? :stately_dutch
28
+ alias :portly? :portly
29
+
30
+ securely_hashes :blathering, :with => AlternateCoder
31
+ securely_hashes :password, :to => :password_hash
32
+ securely_hashes :chicken_fist, :to => :chicken_fist_digest
33
+
34
+ def chicken_fist=(bird)
35
+ @stately_dutch = true
36
+ end
37
+
38
+ def rip_steakface=(beefy)
39
+ @portly = true
40
+ end
41
+
42
+ securely_hashes :rip_steakface, :to => :big_mclargehuge
43
+ end
44
+ end
45
+
46
+ before(:each) do
47
+ HashingModel.delete_all
48
+ end
49
+
50
+ it "should create getters and setters for :password" do
51
+ new_model.should respond_to(:password)
52
+ new_model.should respond_to(:password=)
53
+ new_model.password = 'super duper'
54
+ new_model.password.should == 'super duper'
55
+ end
56
+
57
+ it "should serialize :password to :password_digest on save" do
58
+ new_model.password = 'super duper'
59
+ new_model.save
60
+ new_model.reload
61
+ new_model.password_hash.should_not be_empty
62
+ new_model.password_hash.should be_a_kind_of(BCrypt::Password)
63
+ new_model.password_hash.should == 'super duper'
64
+ end
65
+
66
+ it "should get clobbered by #chicken_fist=" do
67
+ new_model.should_not be_stately_dutch
68
+ new_model.chicken_fist = 'a string to hash'
69
+ new_model.should be_stately_dutch
70
+ end
71
+
72
+ it "should not hash properly because of #chicken_fist=" do
73
+ new_model.chicken_fist = 'a string to hash'
74
+ new_model.save
75
+ new_model.chicken_fist_digest.should be_empty
76
+ end
77
+
78
+ it "should clobber #rip_steakface=" do
79
+ new_model.should_not be_portly
80
+ new_model.rip_steakface = 'a string to hash'
81
+ new_model.should_not be_portly
82
+ end
83
+
84
+ it "should has properly because it overwrote #rip_steakface=" do
85
+ new_model.rip_steakface = 'a string to hash'
86
+ new_model.save
87
+ new_model.big_mclargehuge.should_not be_empty
88
+ end
89
+
90
+ it "should serialize :blathering with an alternate coder" do
91
+ new_model.blathering = 'fancy pants'
92
+ new_model.save
93
+ new_model.reload
94
+ new_model.blathering.should == 'decoded'
95
+ end
96
+ end
@@ -0,0 +1,16 @@
1
+ Dir[File.expand_path('support', File.dirname(__FILE__)) + "/**/*.rb"].each { |f| require f }
2
+
3
+ begin
4
+ require 'simplecov'
5
+ SimpleCov.start do
6
+ add_filter "/spec/"
7
+ end
8
+ rescue LoadError
9
+ end
10
+
11
+ require 'securely_hashed_attributes'
12
+ require 'with_model'
13
+
14
+ RSpec.configure do |config|
15
+ config.extend WithModel
16
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: securely_hashed_attributes
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Ian D. Eccles
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-05-19 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rails
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 3.1.0.beta1
24
+ type: :development
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: 2.6.0
35
+ type: :development
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: with_model
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.1.5
46
+ type: :development
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: sqlite3
50
+ prerelease: false
51
+ requirement: &id004 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ~>
55
+ - !ruby/object:Gem::Version
56
+ version: 1.3.3
57
+ type: :development
58
+ version_requirements: *id004
59
+ - !ruby/object:Gem::Dependency
60
+ name: simplecov
61
+ prerelease: false
62
+ requirement: &id005 !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: 0.4.2
68
+ type: :development
69
+ version_requirements: *id005
70
+ description: Add securely hashed attributes to your models, serialized with BCrypt
71
+ email:
72
+ - ian.eccles@gmail.com
73
+ executables: []
74
+
75
+ extensions: []
76
+
77
+ extra_rdoc_files: []
78
+
79
+ files:
80
+ - .gitignore
81
+ - .rspec
82
+ - Gemfile
83
+ - LICENSE
84
+ - README.md
85
+ - Rakefile
86
+ - init.rb
87
+ - lib/securely_hashed_attributes.rb
88
+ - lib/securely_hashed_attributes/coders.rb
89
+ - lib/securely_hashed_attributes/coders/bcrypt_password_column.rb
90
+ - lib/securely_hashed_attributes/version.rb
91
+ - securely_hashed_attributes.gemspec
92
+ - spec/securely_hashed_attributes/coders/bcrypt_password_column_spec.rb
93
+ - spec/securely_hashed_attributes_spec.rb
94
+ - spec/spec_helper.rb
95
+ homepage: ""
96
+ licenses: []
97
+
98
+ post_install_message:
99
+ rdoc_options: []
100
+
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: "0"
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: "0"
115
+ requirements: []
116
+
117
+ rubyforge_project: securely_hashed_attributes
118
+ rubygems_version: 1.7.2
119
+ signing_key:
120
+ specification_version: 3
121
+ summary: Add securely hashed attributes to your models
122
+ test_files:
123
+ - spec/securely_hashed_attributes/coders/bcrypt_password_column_spec.rb
124
+ - spec/securely_hashed_attributes_spec.rb
125
+ - spec/spec_helper.rb
126
+ has_rdoc: