salt-and-pepper 0.2.2

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
+ *.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
+