Skip to content

Commit 101a48b

Browse files
committed
(MODULES-1737) Add pw_hash() function
1 parent c297bd8 commit 101a48b

File tree

4 files changed

+195
-0
lines changed

4 files changed

+195
-0
lines changed

README.markdown

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,24 @@ Calling the class or definition from outside the current module will fail. For e
500500

501501
*Type*: statement
502502

503+
#### `pw_hash`
504+
505+
Hashes a password using the crypt function. Provides a hash usable on most POSIX systems.
506+
507+
The first argument to this function is the password to hash. If it is undef or an empty string, this function returns undef.
508+
509+
The second argument to this function is which type of hash to use. It will be converted into the appropriate crypt(3) hash specifier. Valid hash types are:
510+
511+
|Hash type |Specifier|
512+
|---------------------|---------|
513+
|MD5 |1 |
514+
|SHA-256 |5 |
515+
|SHA-512 (recommended)|6 |
516+
517+
The third argument to this function is the salt to use.
518+
519+
Note: this uses the Puppet Master's implementation of crypt(3). If your environment contains several different operating systems, ensure that they are compatible before using this function.
520+
503521
#### `range`
504522

505523
When given range in the form of '(start, stop)', `range` extrapolates a range as an array. For example, `range("0", "9")` returns [0,1,2,3,4,5,6,7,8,9]. Zero-padded strings are converted to integers automatically, so `range("00", "09")` returns [0,1,2,3,4,5,6,7,8,9].
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
Puppet::Parser::Functions::newfunction(
2+
:pw_hash,
3+
:type => :rvalue,
4+
:arity => 3,
5+
:doc => "Hashes a password using the crypt function. Provides a hash
6+
usable on most POSIX systems.
7+
8+
The first argument to this function is the password to hash. If it is
9+
undef or an empty string, this function returns undef.
10+
11+
The second argument to this function is which type of hash to use. It
12+
will be converted into the appropriate crypt(3) hash specifier. Valid
13+
hash types are:
14+
15+
|Hash type |Specifier|
16+
|---------------------|---------|
17+
|MD5 |1 |
18+
|SHA-256 |5 |
19+
|SHA-512 (recommended)|6 |
20+
21+
The third argument to this function is the salt to use.
22+
23+
Note: this uses the Puppet Master's implementation of crypt(3). If your
24+
environment contains several different operating systems, ensure that they
25+
are compatible before using this function.") do |args|
26+
raise ArgumentError, "pw_hash(): wrong number of arguments (#{args.size} for 3)" if args.size != 3
27+
raise ArgumentError, "pw_hash(): first argument must be a string" unless args[0].is_a? String or args[0].nil?
28+
raise ArgumentError, "pw_hash(): second argument must be a string" unless args[1].is_a? String
29+
hashes = { 'md5' => '1',
30+
'sha-256' => '5',
31+
'sha-512' => '6' }
32+
hash_type = hashes[args[1].downcase]
33+
raise ArgumentError, "pw_hash(): #{args[1]} is not a valid hash type" if hash_type.nil?
34+
raise ArgumentError, "pw_hash(): third argument must be a string" unless args[2].is_a? String
35+
raise ArgumentError, "pw_hash(): third argument must not be empty" if args[2].empty?
36+
raise ArgumentError, "pw_hash(): characters in salt must be in the set [a-zA-Z0-9./]" unless args[2].match(/\A[a-zA-Z0-9.\/]+\z/)
37+
38+
password = args[0]
39+
return nil if password.empty?
40+
41+
# handle weak implementations of String#crypt
42+
if 'test'.crypt('$1$1') != '$1$1$Bp8CU9Oujr9SSEw53WV6G.'
43+
# JRuby < 1.7.17
44+
if RUBY_PLATFORM == 'java'
45+
# override String#crypt for password variable
46+
def password.crypt(salt)
47+
# puppetserver bundles Apache Commons Codec
48+
org.apache.commons.codec.digest.Crypt.crypt(self.to_java_bytes, salt)
49+
end
50+
else
51+
# MS Windows and other systems that don't support enhanced salts
52+
raise Puppet::ParseError, 'system does not support enhanced salts'
53+
end
54+
end
55+
password.crypt("$#{hash_type}$#{args[2]}")
56+
end

spec/acceptance/pw_hash_spec.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#! /usr/bin/env ruby -S rspec
2+
require 'spec_helper_acceptance'
3+
4+
# Windows and OS X do not have useful implementations of crypt(3)
5+
describe 'pw_hash function', :unless => (UNSUPPORTED_PLATFORMS + ['windows', 'Darwin']).include?(fact('operatingsystem')) do
6+
describe 'success' do
7+
it 'hashes passwords' do
8+
pp = <<-EOS
9+
$o = pw_hash('password', 6, 'salt')
10+
notice(inline_template('pw_hash is <%= @o.inspect %>'))
11+
EOS
12+
13+
apply_manifest(pp, :catch_failures => true) do |r|
14+
expect(r.stdout).to match(/pw_hash is "\$6\$salt\$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy\.g\."/)
15+
end
16+
end
17+
18+
it 'returns nil if no password is provided' do
19+
pp = <<-EOS
20+
$o = pw_hash('', 6, 'salt')
21+
notice(inline_template('pw_hash is <%= @o.inspect %>'))
22+
EOS
23+
24+
apply_manifest(pp, :catch_failures => true) do |r|
25+
expect(r.stdout).to match(/pw_hash is ""/)
26+
end
27+
end
28+
end
29+
describe 'failure' do
30+
it 'handles less than three arguments'
31+
it 'handles more than three arguments'
32+
it 'handles non strings'
33+
end
34+
end

spec/functions/pw_hash_spec.rb

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#! /usr/bin/env ruby -S rspec
2+
require 'spec_helper'
3+
4+
describe "the pw_hash function" do
5+
let(:scope) { PuppetlabsSpec::PuppetInternals.scope }
6+
7+
it "should exist" do
8+
expect(Puppet::Parser::Functions.function("pw_hash")).to eq("function_pw_hash")
9+
end
10+
11+
it "should raise an ArgumentError if there are less than 3 arguments" do
12+
expect { scope.function_pw_hash([]) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
13+
expect { scope.function_pw_hash(['password']) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
14+
expect { scope.function_pw_hash(['password', 'sha-512']) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
15+
end
16+
17+
it "should raise an ArgumentError if there are more than 3 arguments" do
18+
expect { scope.function_pw_hash(['password', 'sha-512', 'salt', 5]) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
19+
end
20+
21+
it "should raise an ArgumentError if the first argument is not a string" do
22+
expect { scope.function_pw_hash([['password'], 'sha-512', 'salt']) }.to( raise_error(ArgumentError, /first argument must be a string/) )
23+
# in Puppet 3, numbers are passed as strings, so we can't test that
24+
end
25+
26+
it "should return nil if the first argument is empty" do
27+
expect(scope.function_pw_hash(['', 'sha-512', 'salt'])).to eq(nil)
28+
end
29+
30+
it "should return nil if the first argument is undef" do
31+
expect(scope.function_pw_hash([nil, 'sha-512', 'salt'])).to eq(nil)
32+
end
33+
34+
it "should raise an ArgumentError if the second argument is an invalid hash type" do
35+
expect { scope.function_pw_hash(['', 'invalid', 'salt']) }.to( raise_error(ArgumentError, /not a valid hash type/) )
36+
end
37+
38+
it "should raise an ArgumentError if the second argument is not a string" do
39+
expect { scope.function_pw_hash(['', [], 'salt']) }.to( raise_error(ArgumentError, /second argument must be a string/) )
40+
end
41+
42+
it "should raise an ArgumentError if the third argument is not a string" do
43+
expect { scope.function_pw_hash(['password', 'sha-512', ['salt']]) }.to( raise_error(ArgumentError, /third argument must be a string/) )
44+
# in Puppet 3, numbers are passed as strings, so we can't test that
45+
end
46+
47+
it "should raise an ArgumentError if the third argument is empty" do
48+
expect { scope.function_pw_hash(['password', 'sha-512', '']) }.to( raise_error(ArgumentError, /third argument must not be empty/) )
49+
end
50+
51+
it "should raise an ArgumentError if the third argument has invalid characters" do
52+
expect { scope.function_pw_hash(['password', 'sha-512', '%']) }.to( raise_error(ArgumentError, /characters in salt must be in the set/) )
53+
end
54+
55+
it "should fail on platforms with weak implementations of String#crypt" do
56+
String.any_instance.expects(:crypt).with('$1$1').returns('$1SoNol0Ye6Xk')
57+
expect { scope.function_pw_hash(['password', 'sha-512', 'salt']) }.to( raise_error(Puppet::ParseError, /system does not support enhanced salts/) )
58+
end
59+
60+
it "should return a hashed password" do
61+
result = scope.function_pw_hash(['password', 'sha-512', 'salt'])
62+
expect(result).to eql('$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.')
63+
end
64+
65+
it "should use the specified salt" do
66+
result = scope.function_pw_hash(['password', 'sha-512', 'salt'])
67+
expect(result).to match('salt')
68+
end
69+
70+
it "should use the specified hash type" do
71+
resultmd5 = scope.function_pw_hash(['password', 'md5', 'salt'])
72+
resultsha256 = scope.function_pw_hash(['password', 'sha-256', 'salt'])
73+
resultsha512 = scope.function_pw_hash(['password', 'sha-512', 'salt'])
74+
75+
expect(resultmd5).to eql('$1$salt$qJH7.N4xYta3aEG/dfqo/0')
76+
expect(resultsha256).to eql('$5$salt$Gcm6FsVtF/Qa77ZKD.iwsJlCVPY0XSMgLJL0Hnww/c1')
77+
expect(resultsha512).to eql('$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.')
78+
end
79+
80+
it "should generate a valid hash" do
81+
password_hash = scope.function_pw_hash(['password', 'sha-512', 'salt'])
82+
83+
hash_parts = password_hash.match(%r{\A\$(.*)\$([a-zA-Z0-9./]+)\$([a-zA-Z0-9./]+)\z})
84+
85+
expect(hash_parts).not_to eql(nil)
86+
end
87+
end

0 commit comments

Comments
 (0)