securely_hashed_attributes 0.1.0

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