salt-and-pepper 0.2.2

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
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in salt-and-pepper.gemspec
4
+ gemspec
data/README.textile ADDED
@@ -0,0 +1,138 @@
1
+ h1. Salt and Pepper
2
+
3
+ Provides automatic password hashing for ActiveRecord (>= 3.0) and a couple of methods for generating random strings, tokens, etc.
4
+ Features:
5
+
6
+ * Mark columns for auto-hashing with a single line of code.
7
+ * Automatic salting of hashes. No separate column is required for the salt.
8
+ * Does not break validations on the hashed columns (only a small change is required).
9
+ * Super easy hash verification.
10
+ * Tested using RSpec 2
11
+
12
+ <code>Digest::SHA256</code> is used for hashing and <code>ActiveRecord::SecureRandom</code> is used for generating random stuff.
13
+
14
+ h2. Installation
15
+
16
+ Just add it to your Gemfile and run <code>bundle install</code>:
17
+
18
+ <pre>
19
+ gem "salt-and-pepper"
20
+ </pre>
21
+
22
+ h2. Usage
23
+
24
+ To enable automatic hashing for a column, call <code>hash_column</code> in your model:
25
+
26
+ <pre>
27
+ class User < ActiveRecord::Base
28
+ hash_column :password
29
+ end
30
+ </pre>
31
+
32
+ You can specify multiple columns in one line or in separate lines:
33
+
34
+ <pre>
35
+ class User < ActiveRecord::Base
36
+ hash_column :password, :security_token
37
+
38
+ # or
39
+ hash_column :password
40
+ hash_column :security_token
41
+ end
42
+ </pre>
43
+
44
+ h3. Options
45
+
46
+ You can pass the <code>:length</code> option to change the length of the stored hash.
47
+ Numbers between 96 and 192 are accepted, the default value is 128. Make sure the database column can store a string that long!
48
+
49
+ <pre>
50
+ # set length for both columns
51
+ hash_column :password, :security_token, :length => 100
52
+
53
+ # or adjust them individually
54
+ hash_column :password, :length => 160
55
+ hash_column :secret, :length => 120
56
+ </pre>
57
+
58
+ By default, blank _(= empty or whitespace-only)_ strings will be converted to <code>nil</code>, and will not be hashed.
59
+ If you _really_ want blank strings to be hashed, use the <code>:hash_blank_strings</code> option:
60
+
61
+ <pre>
62
+ # Default behavior:
63
+ # nil => nil
64
+ # empty string => nil
65
+ # whitespace-only string => nil
66
+
67
+ hash_column :password, :hash_blank_strings => true
68
+
69
+ # New behavior:
70
+ # nil => nil
71
+ # empty string => 77c0a93ad8e5f42cf676...
72
+ # whitespace-only string => 1b8d091174299844b1a4...
73
+ </pre>
74
+
75
+ h3. Verification
76
+
77
+ Just compare the two values:
78
+
79
+ <pre>
80
+ if @user.password == "secret"
81
+ # password is valid
82
+ end
83
+ </pre>
84
+
85
+ A full example:
86
+
87
+ <pre>
88
+ # app/models/user.rb
89
+ class User < ActiveRecord::Base
90
+ hash_column :password
91
+ end
92
+
93
+ # app/controllers/sessions_controller.rb
94
+ def create
95
+ @user = User.find_by_username(params[:username])
96
+ if @user.present? && @user.password == params[:password]
97
+ # login user here
98
+ else
99
+ redirect_to new_session_path, :alert => "Invalid username or password."
100
+ end
101
+ end
102
+ </pre>
103
+
104
+ h3. Validating hashed columns
105
+
106
+ Salt and Pepper provides the <code>validate_[column]?</code> method for deciding whether validations on the column should be performed.
107
+ Use it to prevent running your sophisticated length-checking algorithms on a 128-character hash :). Skipping validation of hashed values is safe because they were already checked at the time they were set.
108
+
109
+ <pre>
110
+ class User < ActiveRecord::Base
111
+ encrypt :password
112
+ validates :password, :length => { :within => 6..100 }, :if => :validate_password?
113
+ end
114
+ </pre>
115
+
116
+ h3. Generating random stuff
117
+
118
+ Salt and Pepper has a couple of handy methods for generating random numbers, codes, tokens, etc:
119
+
120
+ <pre>
121
+ SaltPepper.number(6) # => 4 (identical to: 0..5)
122
+ SaltPepper.number(10..20) # => 11
123
+ SaltPepper.alpha_code # => "SNPBJSDG"
124
+ SaltPepper.alpha_code(4) # => "FKNP"
125
+ SaltPepper.numeric_code # => "01570475"
126
+ SaltPepper.numeric_code(20) # => "70110124996934848762"
127
+ SaltPepper.code # => "29Y3WSEC" (alphanumeric)
128
+ SaltPepper.code(5) # => "89U1F"
129
+ SaltPepper.code(10, 'a'..'z') # => "mqxeozlelw"
130
+ SaltPepper.code(15, (0..1).to_a + ('a'..'b').to_a) # => "0ab1b0b1b01a0a1"
131
+ SaltPepper.token # => "a0d5828f79e9e22dbc1f896e49f8183a"
132
+ SaltPepper.token(16) # => "caa4a085edb19499"
133
+ </pre>
134
+
135
+ h2. License
136
+
137
+ Released under the MIT license.
138
+ Copyright (C) Máté Solymosi 2011
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,12 @@
1
+ require "active_support"
2
+ require "active_support/core_ext"
3
+ require "active_record"
4
+
5
+ require "salt_pepper/random"
6
+ require "salt_pepper/hashed_string"
7
+ require "salt_pepper/model_extensions"
8
+
9
+ module SaltPepper
10
+ class ArgumentError < ArgumentError; end
11
+ class ValueHashedError < ArgumentError; end
12
+ end
@@ -0,0 +1,99 @@
1
+ module SaltPepper
2
+ class HashedString
3
+
4
+ DefaultOptions = { :length => 128 }
5
+ Length = 96..192
6
+
7
+ attr_reader :hsh, :salt
8
+
9
+ def initialize(str, options = DefaultOptions)
10
+ raise ArgumentError, "Input must be a String" unless str.is_a?(String)
11
+ options.reverse_merge! DefaultOptions
12
+ HashedString.check_options! options
13
+ @salt = SaltPepper::Random.salt(options[:length])
14
+ @hsh = HashedString.hsh(str, @salt)
15
+ end
16
+
17
+ def to_s
18
+ ""
19
+ end
20
+
21
+ def to_yaml
22
+ result
23
+ end
24
+
25
+ def result
26
+ hsh + salt
27
+ end
28
+
29
+ def inspect
30
+ result.inspect
31
+ end
32
+
33
+ def ==(obj)
34
+ return self.eql?(obj) if obj.is_a?(HashedString)
35
+ return HashedString.hsh(obj, @salt) == @hsh if obj.is_a?(String)
36
+ false
37
+ end
38
+
39
+ def eql?(other)
40
+ result == other.result
41
+ end
42
+
43
+ def set_attributes(h, s)
44
+ @hsh, @salt = h, s
45
+ end
46
+
47
+ def self.from_hash(h)
48
+ raise ArgumentError, "Hash must be a String or a HashedString" unless h.is_a?(String) || h.is_a?(HashedString)
49
+ h = h.result if h.is_a?(HashedString)
50
+ raise ArgumentError, "Length should be within #{Length.inspect}" unless Length.include?(h.length)
51
+ hs = h[0...64]
52
+ sl = h[64...h.length]
53
+ n = HashedString.new("")
54
+ n.set_attributes(hs, sl)
55
+ n
56
+ end
57
+
58
+ def length
59
+ self.class.raise_value_hashed_error
60
+ end
61
+
62
+ def =~(regexp)
63
+ self.class.raise_value_hashed_error
64
+ end
65
+
66
+ protected
67
+
68
+ def self.hsh(password, salt)
69
+ Digest::SHA256.hexdigest(password + salt)
70
+ end
71
+
72
+ private
73
+
74
+ def self.check_options!(options)
75
+ options.each do |name, value|
76
+ case name.to_s
77
+ when "length" then
78
+ raise ArgumentError, "Length should be a Fixnum" unless value.is_a?(Fixnum)
79
+ raise ArgumentError, "Length should be within #{Length.inspect}" unless Length.include?(value)
80
+ else
81
+ raise ArgumentError, "Invalid option: #{name.to_s}"
82
+ end
83
+ end
84
+ true
85
+ end
86
+
87
+ def self.raise_value_hashed_error
88
+ raise SaltPepper::ValueHashedError, "This attribute is currently hashed, so it cannot be accessed or validated. Add :if => :validate_[column name]? to your validator to prevent it from running when the attribute is hashed."
89
+ end
90
+
91
+ end
92
+ end
93
+
94
+ class String
95
+ def ==(obj)
96
+ return obj == self if obj.is_a?(SaltPepper::HashedString)
97
+ super
98
+ end
99
+ end
@@ -0,0 +1,82 @@
1
+ module SaltPepper
2
+ module ModelExtensions
3
+
4
+ DefaultHashColumnOptions = { :length => SaltPepper::HashedString::DefaultOptions[:length], :hash_blank_strings => false }
5
+
6
+ module ClassMethods
7
+
8
+ def hashed_columns
9
+ read_inheritable_attribute :hashed_columns
10
+ end
11
+
12
+ def hash_column(*args)
13
+ options = args.extract_options!
14
+ options.keys.each { |k| raise ArgumentError, "Invalid option: #{k.to_s}" unless DefaultHashColumnOptions.keys.include?(k.to_sym) }
15
+ raise ArgumentError, "No columns specified" if args.empty?
16
+ raise ArgumentError, "Primary key cannot be hashed" if (["id", self.primary_key] - (args.map { |a| a.to_s })).length < 2
17
+ args.each do |arg|
18
+ raise ArgumentError, "Column name should be a symbol or a string" unless arg.is_a?(String) || arg.is_a?(Symbol)
19
+ raise ArgumentError, "'#{arg.to_s}' is not a valid column name" unless self.column_names.include?(arg.to_s)
20
+
21
+ write_inheritable_hash(:hashed_columns, { arg.to_sym => options.symbolize_keys.reverse_merge(DefaultHashColumnOptions) })
22
+ self.cached_attributes.delete arg.to_s
23
+
24
+ self.class_eval <<-EVAL
25
+ def validate_#{arg.to_s}?
26
+ !hashed?("#{arg.to_s}")
27
+ end
28
+ EVAL
29
+ end
30
+ if !self._save_callbacks.map { |c| c.filter.to_sym }.include?(:hash_before_save)
31
+ self.before_save :hash_before_save
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ def self.included(klass)
38
+ klass.extend(ClassMethods)
39
+ klass.write_inheritable_hash(:hashed_columns, {})
40
+ klass.instance_eval <<-EVAL
41
+ after_initialize :initialize_hashed_columns
42
+ EVAL
43
+ end
44
+
45
+ private
46
+
47
+ def hash_before_save
48
+ self.class.hashed_columns.each do |column, options|
49
+ next if hashed?(column)
50
+ @attributes[column.to_s] = nil if read_attribute(column).blank? && !options[:hash_blank_strings]
51
+ perform_hashing_on_column(column, options[:length]) unless read_attribute(column).nil?
52
+ end
53
+ end
54
+
55
+ def perform_hashing_on_column(column, length)
56
+ @attributes[column.to_s] = HashedString.new(read_attribute(column), :length => length)
57
+ end
58
+
59
+ def convert_column_to_hashed_string(column)
60
+ @attributes[column.to_s] = HashedString.from_hash(read_attribute(column))
61
+ end
62
+
63
+ def hashed?(column)
64
+ read_attribute(column.to_s).is_a?(HashedString)
65
+ end
66
+
67
+ def initialize_hashed_columns
68
+ unless self.new_record?
69
+ self.class.hashed_columns.keys.each do |k|
70
+ convert_column_to_hashed_string(k) unless read_attribute(k).blank?
71
+ end
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+
78
+ ActiveSupport.on_load :active_record do
79
+ class ActiveRecord::Base
80
+ include SaltPepper::ModelExtensions
81
+ end
82
+ end
@@ -0,0 +1,46 @@
1
+ module SaltPepper
2
+ module Random
3
+
4
+ def self.token(size = 32)
5
+ hex(size)
6
+ end
7
+
8
+ def self.code(size = 8, chars = ('A'..'Z').to_a + (0..9).to_a)
9
+ chars = chars.to_a if chars.is_a?(Range)
10
+ chars = chars.chars.to_a.uniq if chars.is_a?(String)
11
+ (1..size).map { chars[self.number(chars.length)] }.join
12
+ end
13
+
14
+ def self.numeric_code(size = 8)
15
+ self.code(size, 0..9)
16
+ end
17
+
18
+ def self.alpha_code(size = 8)
19
+ self.code(size, 'A'..'Z')
20
+ end
21
+
22
+ def self.number(max_or_range)
23
+ return max_or_range.begin + ActiveSupport::SecureRandom.random_number(max_or_range.end - max_or_range.begin + 1) if max_or_range.is_a?(Range)
24
+ ActiveSupport::SecureRandom.random_number(max_or_range)
25
+ end
26
+
27
+ private
28
+
29
+ def self.salt(size = DefaultOptions[:length])
30
+ hex(size - 64)
31
+ end
32
+
33
+ def self.hex(size)
34
+ ActiveSupport::SecureRandom.hex(size / 2)
35
+ end
36
+
37
+ end
38
+
39
+ (Random.methods(false) - Random.private_methods(false)).each do |m|
40
+ module_eval <<-EVAL, __FILE__, __LINE__
41
+ def self.#{m}(*args)
42
+ Random.#{m}(*args)
43
+ end
44
+ EVAL
45
+ end
46
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module SaltPepper
2
+ VERSION = "0.2.2"
3
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "salt-and-pepper"
7
+ s.version = SaltPepper::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Mate Solymosi"]
10
+ s.email = ["mate@solymosi.eu"]
11
+ s.homepage = "http://github.com/SMWEB/salt-and-pepper"
12
+ s.summary = %q{Super easy password salting and hashing for ActiveRecord (Rails)}
13
+ s.description = %q{Super easy password salting and hashing for ActiveRecord (Rails)}
14
+
15
+ s.rubyforge_project = "salt-and-pepper"
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("activesupport", ">= 3.0.0")
23
+ s.add_dependency("activerecord", ">= 3.0.0")
24
+ end
@@ -0,0 +1,151 @@
1
+ require File.expand_path('spec_helper.rb', File.dirname(__FILE__))
2
+
3
+ describe SaltPepper::HashedString do
4
+
5
+ before(:each) do
6
+ @hash = SaltPepper::HashedString.new("secret")
7
+ end
8
+
9
+ describe "initialize" do
10
+
11
+ it "raises error invalid parameters" do
12
+ lambda { SaltPepper::HashedString.new(nil) }.should raise_error(SaltPepper::ArgumentError)
13
+ lambda { SaltPepper::HashedString.new(3) }.should raise_error(SaltPepper::ArgumentError)
14
+ lambda { SaltPepper::HashedString.new([]) }.should raise_error(SaltPepper::ArgumentError)
15
+ end
16
+
17
+ it "accepts valid parameters" do
18
+ lambda { SaltPepper::HashedString.new("") }.should_not raise_error(SaltPepper::ArgumentError)
19
+ lambda { SaltPepper::HashedString.new("secret") }.should_not raise_error(SaltPepper::ArgumentError)
20
+ end
21
+
22
+ it "generates a valid salt with default length" do
23
+ @hash.salt.length.should == SaltPepper::HashedString::DefaultOptions[:length] - 64
24
+ end
25
+
26
+ it "generates a valid salt with custom length" do
27
+ SaltPepper::HashedString.new("secret", :length => 192).salt.length.should == 128
28
+ end
29
+
30
+ it "raises error for invalid options" do
31
+ lambda { SaltPepper::HashedString.new("secret", :length => 95) }.should raise_error(SaltPepper::ArgumentError)
32
+ lambda { SaltPepper::HashedString.new("secret", :length => 197) }.should raise_error(SaltPepper::ArgumentError)
33
+ lambda { SaltPepper::HashedString.new("secret", :length => true) }.should raise_error(SaltPepper::ArgumentError)
34
+ lambda { SaltPepper::HashedString.new("secret", :length => "oops") }.should raise_error(SaltPepper::ArgumentError)
35
+ lambda { SaltPepper::HashedString.new("secret", :oops => true) }.should raise_error(SaltPepper::ArgumentError)
36
+ end
37
+
38
+ it "hashes the input string" do
39
+ @hash.hsh.should_not be_blank
40
+ end
41
+
42
+ end
43
+
44
+ describe "hsh" do
45
+ it "returns the hash" do
46
+ @hash.hsh.should == @hash.result[0...64]
47
+ end
48
+ end
49
+
50
+ describe "salt" do
51
+ it "returns the salt" do
52
+ @hash.salt.should == @hash.result[64...128]
53
+ end
54
+ end
55
+
56
+ describe "to_s" do
57
+ it "returns empty string" do
58
+ @hash.to_s.should == ""
59
+ end
60
+ end
61
+
62
+ describe "to_yaml" do
63
+ it "returns the result" do
64
+ @hash.to_yaml.should == @hash.result
65
+ end
66
+ end
67
+
68
+ describe "inspect" do
69
+ it "returns result.inspect" do
70
+ @hash.inspect.should == @hash.result.inspect
71
+ end
72
+ end
73
+
74
+ describe "set_attributes" do
75
+ it "sets attributes correctly" do
76
+ @hash.set_attributes("hehe", "haha")
77
+ @hash.hsh.should == "hehe"
78
+ @hash.salt.should == "haha"
79
+ end
80
+ end
81
+
82
+ describe "==" do
83
+
84
+ it "returns true if String is given and verification succeeds" do
85
+ @hash.should == "secret"
86
+ end
87
+
88
+ it "returns false if String is given and verification fails" do
89
+ @hash.should_not == "oops"
90
+ end
91
+
92
+ it "returns true if another HashedString is given and they match" do
93
+ h = SaltPepper::HashedString.from_hash(@hash)
94
+ @hash.should == h
95
+ end
96
+
97
+ it "returns false if another HashedString is given but they don't match" do
98
+ h = SaltPepper::HashedString.new("oops")
99
+ @hash.should_not == h
100
+ end
101
+
102
+ it "returns false if something other than a String or HashedString is given" do
103
+ @hash.should_not == 10
104
+ @hash.should_not == []
105
+ @hash.should_not == {}
106
+ end
107
+
108
+ end
109
+
110
+ describe "from_hash" do
111
+
112
+ before(:each) do
113
+ @f = SaltPepper::HashedString.from_hash(@hash)
114
+ end
115
+
116
+ it "raises error for invalid parameters" do
117
+ lambda { SaltPepper::HashedString.from_hash(nil) }.should raise_error(SaltPepper::ArgumentError)
118
+ lambda { SaltPepper::HashedString.from_hash(10) }.should raise_error(SaltPepper::ArgumentError)
119
+ lambda { SaltPepper::HashedString.from_hash("o" * 95) }.should raise_error(SaltPepper::ArgumentError)
120
+ lambda { SaltPepper::HashedString.from_hash("o" * 197) }.should raise_error(SaltPepper::ArgumentError)
121
+ end
122
+
123
+ it "separates the hash and the salt correctly" do
124
+ @f.hsh.should == @hash.hsh
125
+ @f.salt.should == @hash.salt
126
+ @f.should == @hash
127
+ @f.should == "secret"
128
+ end
129
+
130
+ end
131
+
132
+ describe "eql?" do
133
+ it "works correctly" do
134
+ SaltPepper::HashedString.from_hash(@hash.result).eql?(@hash).should == true
135
+ end
136
+ end
137
+
138
+ describe "String extension" do
139
+ it "works correctly" do
140
+ "secret".should == @hash
141
+ end
142
+ end
143
+
144
+ describe "validation help messages" do
145
+ it "work correctly" do
146
+ lambda { @hash.length }.should raise_error(SaltPepper::ValueHashedError)
147
+ lambda { @hash =~ /.*/ }.should raise_error(SaltPepper::ValueHashedError)
148
+ end
149
+ end
150
+
151
+ end
@@ -0,0 +1,275 @@
1
+ require File.expand_path('spec_helper.rb', File.dirname(__FILE__))
2
+
3
+ describe SaltPepper::ModelExtensions do
4
+
5
+ before(:each) do
6
+ ActiveRecord::Base.establish_connection :adapter => "sqlite3", :database => ":memory:"
7
+ ActiveRecord::Base.connection.create_table :users do |t|
8
+ t.string :password
9
+ t.string :token
10
+ t.string :required, :null => false, :default => "something"
11
+ end
12
+
13
+ class User; end
14
+
15
+ @user = Class.new(ActiveRecord::Base) do
16
+ self.table_name = "users"
17
+ @_model_name = ActiveModel::Name.new(User)
18
+ end
19
+ end
20
+
21
+ describe "included?" do
22
+
23
+ it "should have SaltPepper::ModelExtensions included" do
24
+ @user.included_modules.include?(SaltPepper::ModelExtensions)
25
+ end
26
+
27
+ it "should add the hash_column class method to the class it's included in" do
28
+ @user.respond_to?("hash_column").should == true
29
+ end
30
+
31
+ end
32
+
33
+ describe "hash_column" do
34
+
35
+ it "should add a single attribute to the list" do
36
+ @user.hash_column :password
37
+ @user.hashed_columns.count.should == 1
38
+ @user.hashed_columns.keys.include?(:password).should == true
39
+ end
40
+
41
+ it "should add multiple attributes to the list" do
42
+ @user.hash_column :password, :token
43
+ @user.hashed_columns.count.should == 2
44
+ @user.hashed_columns.keys.include?(:password).should == true
45
+ @user.hashed_columns.keys.include?(:token).should == true
46
+ end
47
+
48
+ it "should work properly when called multiple times" do
49
+ @user.hash_column :password
50
+ @user.hash_column :token
51
+ @user.hashed_columns.count.should == 2
52
+ @user.hashed_columns.keys.include?(:password).should == true
53
+ @user.hashed_columns.keys.include?(:token).should == true
54
+ end
55
+
56
+ it "should allow valid parameters" do
57
+ @user.hash_column :password, :length => 100, :hash_blank_strings => true
58
+ @user.hashed_columns.count.should == 1
59
+ @user.hashed_columns[:password].should == { :length => 100, :hash_blank_strings => true }
60
+ end
61
+
62
+ it "should apply defaults for unset options" do
63
+ @user.hash_column :password
64
+ @user.hashed_columns[:password].should == SaltPepper::ModelExtensions::DefaultHashColumnOptions
65
+ end
66
+
67
+ it "should not add the same attribute twice" do
68
+ 2.times { @user.hash_column :password }
69
+ @user.hashed_columns.count.should == 1
70
+ end
71
+
72
+ it "should not add an attribute without an existing column to the list" do
73
+ lambda { @user.hash_column :oops }.should raise_error(ArgumentError)
74
+ @user.hashed_columns.should be_empty
75
+ end
76
+
77
+ it "should not allow invalid parameters" do
78
+ lambda { @user.hash_column :password, :oops => true }.should raise_error(ArgumentError)
79
+ end
80
+
81
+ it "should not allow primary key column to be hashed" do
82
+ lambda { @user.hash_column :id }.should raise_error(ArgumentError)
83
+ end
84
+
85
+ it "should delete attribute from cached attributes list when present" do
86
+ @user.cached_attributes.add "password"
87
+ @user.hash_column :password
88
+ @user.cached_attributes.include?("password").should == false
89
+ end
90
+
91
+ end
92
+
93
+ describe "after_initialize" do
94
+
95
+ before(:each) do
96
+ @user.hash_column :password
97
+ @u = @user.new
98
+ end
99
+
100
+ it "should replace the hashed values to a HashedString if not empty and not new_record?" do
101
+ @u.password = "secret"
102
+ @u.save!
103
+ @u = @user.first
104
+ @u.password.is_a?(SaltPepper::HashedString).should == true
105
+ @u.password.respond_to?(:result).should == true
106
+ @u.password.should == "secret"
107
+ end
108
+
109
+ it "should not replace empty values to a HashedString" do
110
+ @u.password.is_a?(SaltPepper::HashedString).should == false
111
+ @u.save!
112
+ @u = @user.first
113
+ @u.password.is_a?(SaltPepper::HashedString).should == false
114
+ end
115
+
116
+ end
117
+
118
+ describe "column hashing on save" do
119
+
120
+ it "hashes column on save" do
121
+ @user.hash_column :password
122
+ @u = @user.new
123
+ @u.password.should == nil
124
+ @u.validate_password?.should == true
125
+ @u.password = "secret"
126
+ @u.password.class.should == String
127
+ @u.password.should == "secret"
128
+ @u.validate_password?.should == true
129
+ @u.save!
130
+ @u.password.class.should == SaltPepper::HashedString
131
+ @u.password.should == "secret"
132
+ @u.password.should_not == "oops"
133
+ @u.validate_password?.should == false
134
+ end
135
+
136
+ it "does not rehash column if its value was not changed" do
137
+ @user.hash_column :password
138
+ @u = @user.new
139
+ @u.password = "secret"
140
+ @u.save!
141
+ @u.validate_password?.should == false
142
+ @u.save!
143
+ @u.validate_password?.should == false
144
+ @u.password.should == "secret"
145
+ @u.password = @u.password
146
+ @u.validate_password?.should == false
147
+ @u.save!
148
+ @u.password.should == "secret"
149
+ end
150
+
151
+ it "does not hash column if it is nil or blank" do
152
+ @user.hash_column :password
153
+ @u = @user.new
154
+ @u.save!
155
+ @u.password.should be_nil
156
+ @u.validate_password?.should == true
157
+ @u.password = ""
158
+ @u.save!
159
+ @u.password.should be_nil
160
+ @u.validate_password?.should == true
161
+ @u.password = "secret"
162
+ @u.save!
163
+ @u.validate_password?.should == false
164
+ @u.password.should == "secret"
165
+ @u.password = ""
166
+ @u.save!
167
+ @u.password.should be_nil
168
+ @u.validate_password?.should == true
169
+ @u.password = nil
170
+ @u.save!
171
+ @u.password.should be_nil
172
+ @u.validate_password?.should == true
173
+ end
174
+
175
+ it "correctly hashes columns with :hash_blank_strings => true" do
176
+ @user.hash_column :password, :hash_blank_strings => true
177
+ @u = @user.new
178
+ @u.password = nil
179
+ @u.save!
180
+ @u.password.should be_nil
181
+ @u.validate_password?.should == true
182
+ @u.password = "secret"
183
+ @u.save!
184
+ @u.validate_password?.should == false
185
+ @u.password = ""
186
+ @u.save!
187
+ @u.password.should == ""
188
+ @u.password.class.should == SaltPepper::HashedString
189
+ end
190
+
191
+ it "works with validations on the column" do
192
+ @user.hash_column :password
193
+ @user.validates :password, :presence => true, :length => { :within => 5..10 }, :if => :validate_password?
194
+ @u = @user.new
195
+ @u.password = ""
196
+ @u.save.should == false
197
+ @u.password = "abc"
198
+ @u.save.should == false
199
+ @u.password = "ooooooooops"
200
+ @u.save.should == false
201
+ @u.password = "secret"
202
+ @u.save.should == true
203
+ @u.save.should == true
204
+ @u.password = @u.password
205
+ @u.save.should == true
206
+ @u = @user.first
207
+ @u.save.should == true
208
+ @u.password = "abc"
209
+ @u.save.should == false
210
+ end
211
+
212
+ it "works with multiple columns" do
213
+ @user.hash_column :password, :token
214
+ @u = @user.new
215
+ @u.password = "secret"
216
+ @u.save!
217
+ @u.password.should == "secret"
218
+ @u.token.should be_nil
219
+ @u.token = "other"
220
+ @u.save!
221
+ @u.password.should == "secret"
222
+ @u.token.should == "other"
223
+ @u.save!
224
+ @u.password.should == "secret"
225
+ @u.token.should == "other"
226
+ end
227
+
228
+ it "remains in a consistent state if saving fails" do
229
+ @user.hash_column :password
230
+ @u = @user.new
231
+ @u.required = nil
232
+ @u.password = "secret"
233
+ @u.save! rescue nil
234
+ @u.password.should == "secret"
235
+ @u.validate_password?.should == false
236
+ @u.required = "something"
237
+ @u.save!
238
+ @u.password.should == "secret"
239
+ @u.validate_password?.should == false
240
+ end
241
+
242
+ it "works with finds" do
243
+ @user.hash_column :password
244
+ @u = @user.new
245
+ @u.password = "secret"
246
+ @u.save
247
+ @v = @user.first
248
+ @v.validate_password?.should == false
249
+ @u.password.should == "secret"
250
+ @v.password = nil
251
+ @v.save
252
+ @w = @user.first
253
+ @w.validate_password?.should == true
254
+ @w.password.should be_nil
255
+ end
256
+
257
+ it "works with change tracking" do
258
+ @user.hash_column :password
259
+ @u = @user.new
260
+ @u.password = "secret"
261
+ @u.save
262
+ @u = @user.first
263
+ @u.changed_attributes.count.should be_zero
264
+ @u.password = @u.password
265
+ @u.reset_password!
266
+ @u.password.class.should == SaltPepper::HashedString
267
+ @u.password = "other"
268
+ @u.reset_password!
269
+ @u.password.class.should == SaltPepper::HashedString
270
+ @u.validate_password?.should == false
271
+ end
272
+
273
+ end
274
+
275
+ end
@@ -0,0 +1,79 @@
1
+ require File.expand_path('spec_helper.rb', File.dirname(__FILE__))
2
+
3
+ describe SaltPepper::Random do
4
+
5
+ describe "number" do
6
+
7
+ it "(most likely) works when only a max value is specified" do
8
+ 100.times { (0..1).include?(SaltPepper::Random.number(2)).should == true }
9
+ end
10
+
11
+ it "(most likely) works when a range is specified" do
12
+ 100.times { (9..10).include?(SaltPepper::Random.number(9..10)).should == true }
13
+ end
14
+
15
+ end
16
+
17
+ describe "code" do
18
+
19
+ it "works without any parameters" do
20
+ SaltPepper::Random.code.length.should == 8
21
+ end
22
+
23
+ it "returns uppercase alphanumeric characters" do
24
+ SaltPepper::Random.code(1000).should =~ /\A[A-Z0-9]+\z/
25
+ end
26
+
27
+ it "works with a size parameter" do
28
+ SaltPepper::Random.code(10).length.should == 10
29
+ end
30
+
31
+ it "works with size and chars parameters" do
32
+ SaltPepper::Random.code(100, 0..1).chars.to_a.uniq.sort.should == ["0", "1"].sort
33
+ end
34
+
35
+ end
36
+
37
+ describe "numeric_code" do
38
+
39
+ it "returns numbers only" do
40
+ SaltPepper::Random.numeric_code(100).should =~ /\A[0-9]+\z/
41
+ end
42
+
43
+ end
44
+
45
+ describe "alpha_code" do
46
+
47
+ it "returns uppercase letters only" do
48
+ SaltPepper::Random.alpha_code(100).should =~ /\A[A-Z]+\z/
49
+ end
50
+
51
+ end
52
+
53
+ describe "token" do
54
+
55
+ it "works without parameters" do
56
+ SaltPepper::Random.token.length.should == 32
57
+ end
58
+
59
+ it "works with a size parameter" do
60
+ SaltPepper::Random.token(100).length.should == 100
61
+ end
62
+
63
+ it "returns hex characters" do
64
+ SaltPepper::Random.token(1000).should =~ /\A[a-f0-9]*\z/
65
+ end
66
+
67
+ end
68
+
69
+ describe "methods" do
70
+ it "are available on SaltPepper itself" do
71
+ SaltPepper.respond_to?(:number).should == true
72
+ SaltPepper.respond_to?(:code).should == true
73
+ SaltPepper.respond_to?(:alpha_code).should == true
74
+ SaltPepper.respond_to?(:numeric_code).should == true
75
+ SaltPepper.respond_to?(:token).should == true
76
+ end
77
+ end
78
+
79
+ end
@@ -0,0 +1 @@
1
+ require 'salt-and-pepper'
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: salt-and-pepper
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 2
10
+ version: 0.2.2
11
+ platform: ruby
12
+ authors:
13
+ - Mate Solymosi
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-04-24 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activesupport
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 3
32
+ - 0
33
+ - 0
34
+ version: 3.0.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: activerecord
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 7
46
+ segments:
47
+ - 3
48
+ - 0
49
+ - 0
50
+ version: 3.0.0
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ description: Super easy password salting and hashing for ActiveRecord (Rails)
54
+ email:
55
+ - mate@solymosi.eu
56
+ executables: []
57
+
58
+ extensions: []
59
+
60
+ extra_rdoc_files: []
61
+
62
+ files:
63
+ - .gitignore
64
+ - Gemfile
65
+ - README.textile
66
+ - Rakefile
67
+ - lib/salt-and-pepper.rb
68
+ - lib/salt_pepper/hashed_string.rb
69
+ - lib/salt_pepper/model_extensions.rb
70
+ - lib/salt_pepper/random.rb
71
+ - lib/version.rb
72
+ - salt-and-pepper.gemspec
73
+ - spec/hashed_string_spec.rb
74
+ - spec/model_extensions_spec.rb
75
+ - spec/random_spec.rb
76
+ - spec/spec_helper.rb
77
+ has_rdoc: true
78
+ homepage: http://github.com/SMWEB/salt-and-pepper
79
+ licenses: []
80
+
81
+ post_install_message:
82
+ rdoc_options: []
83
+
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 3
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ hash: 3
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ requirements: []
105
+
106
+ rubyforge_project: salt-and-pepper
107
+ rubygems_version: 1.6.2
108
+ signing_key:
109
+ specification_version: 3
110
+ summary: Super easy password salting and hashing for ActiveRecord (Rails)
111
+ test_files: []
112
+