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 +4 -0
- data/Gemfile +4 -0
- data/README.textile +138 -0
- data/Rakefile +4 -0
- data/lib/salt-and-pepper.rb +12 -0
- data/lib/salt_pepper/hashed_string.rb +99 -0
- data/lib/salt_pepper/model_extensions.rb +82 -0
- data/lib/salt_pepper/random.rb +46 -0
- data/lib/version.rb +3 -0
- data/salt-and-pepper.gemspec +24 -0
- data/spec/hashed_string_spec.rb +151 -0
- data/spec/model_extensions_spec.rb +275 -0
- data/spec/random_spec.rb +79 -0
- data/spec/spec_helper.rb +1 -0
- metadata +112 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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,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,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
|
data/spec/random_spec.rb
ADDED
|
@@ -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
|
data/spec/spec_helper.rb
ADDED
|
@@ -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
|
+
|