Skip to content

Commit d1b6dfa

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

File tree

4 files changed

+181
-0
lines changed

4 files changed

+181
-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 a specifier of which hash type to use. Valid hash types are:
510+
511+
|Specifier|Hash type |
512+
|---------|---------------------|
513+
|1 |MD5 |
514+
|5 |SHA-256 |
515+
|6 |SHA-512 (recommended)|
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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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 a specifier of which hash type
12+
to use. Valid hash types are:
13+
14+
|Specifier|Hash type |
15+
|---------|---------------------|
16+
|1 |MD5 |
17+
|5 |SHA-256 |
18+
|6 |SHA-512 (recommended)|
19+
20+
The third argument to this function is the salt to use.
21+
22+
Note: this uses the Puppet Master's implementation of crypt(3). If your
23+
environment contains several different operating systems, ensure that they
24+
are compatible before using this function.") do |args|
25+
raise ArgumentError, "pw_hash(): wrong number of arguments (#{args.size} for 3)" if args.size != 3
26+
raise ArgumentError, "pw_hash(): first argument must be a string" unless args[0].is_a? String
27+
raise ArgumentError, "pw_hash(): #{args[1]} is not a valid hash type" unless ['1', '5', '6'].include? args[1].to_s
28+
raise ArgumentError, "pw_hash(): third argument must be a string" unless args[2].is_a? String
29+
raise ArgumentError, "pw_hash(): third argument must not be empty" if args[2].empty?
30+
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/)
31+
32+
password = args[0].to_s
33+
return nil if password.empty?
34+
35+
# handle weak implementations of String#crypt
36+
if 'test'.crypt('$1$1') != '$1$1$Bp8CU9Oujr9SSEw53WV6G.'
37+
# JRuby < 1.7.17
38+
if RUBY_PLATFORM == 'java'
39+
# override String#crypt for password variable
40+
def password.crypt(salt)
41+
# puppetserver bundles Apache Commons Codec
42+
org.apache.commons.codec.digest.Crypt.crypt(self.to_java_bytes, salt)
43+
end
44+
else
45+
# MS Windows and other systems that don't support enhanced salts
46+
raise Puppet::ParseError, 'system does not support enhanced salts'
47+
end
48+
end
49+
password.crypt("$#{args[1]}$#{args[2]}")
50+
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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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', 6]) }.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', 6, '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'], 6, '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(['', 6, 'salt'])).to eq(nil)
28+
end
29+
30+
it "should raise an ArgumentError if the second argument is an invalid hash type" do
31+
expect { scope.function_pw_hash(['', 3, 'salt']) }.to( raise_error(ArgumentError, /not a valid hash type/) )
32+
end
33+
34+
it "should raise an ArgumentError if the third argument is not a string" do
35+
expect { scope.function_pw_hash(['password', 6, ['salt']]) }.to( raise_error(ArgumentError, /third argument must be a string/) )
36+
# in Puppet 3, numbers are passed as strings, so we can't test that
37+
end
38+
39+
it "should raise an ArgumentError if the third argument is empty" do
40+
expect { scope.function_pw_hash(['password', 6, '']) }.to( raise_error(ArgumentError, /third argument must not be empty/) )
41+
end
42+
43+
it "should raise an ArgumentError if the third argument has invalid characters" do
44+
expect { scope.function_pw_hash(['password', 6, '%']) }.to( raise_error(ArgumentError, /characters in salt must be in the set/) )
45+
end
46+
47+
it "should fail on platforms with weak implementations of String#crypt" do
48+
String.any_instance.expects(:crypt).with('$1$1').returns('$1SoNol0Ye6Xk')
49+
expect { scope.function_pw_hash(['password', 6, 'salt']) }.to( raise_error(Puppet::ParseError, /system does not support enhanced salts/) )
50+
end
51+
52+
it "should return a hashed password" do
53+
result = scope.function_pw_hash(['password', 6, 'salt'])
54+
expect(result).to eql('$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.')
55+
end
56+
57+
it "should use the specified salt" do
58+
result = scope.function_pw_hash(['password', 6, 'salt'])
59+
expect(result).to match('salt')
60+
end
61+
62+
it "should use the specified hash type" do
63+
result1 = scope.function_pw_hash(['password', 1, 'salt'])
64+
result5 = scope.function_pw_hash(['password', 5, 'salt'])
65+
result6 = scope.function_pw_hash(['password', 6, 'salt'])
66+
67+
expect(result1).to eql('$1$salt$qJH7.N4xYta3aEG/dfqo/0')
68+
expect(result5).to eql('$5$salt$Gcm6FsVtF/Qa77ZKD.iwsJlCVPY0XSMgLJL0Hnww/c1')
69+
expect(result6).to eql('$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.')
70+
end
71+
72+
it "should generate a valid hash" do
73+
password_hash = scope.function_pw_hash(['password', 6, 'salt'])
74+
75+
hash_parts = password_hash.match(%r{\A\$(.*)\$([a-zA-Z0-9./]+)\$([a-zA-Z0-9./]+)\z})
76+
77+
expect(hash_parts).not_to eql(nil)
78+
end
79+
end

0 commit comments

Comments
 (0)