Skip to content

Commit

Permalink
(MODULES-1737) Add pw_hash() function
Browse files Browse the repository at this point in the history
  • Loading branch information
elyscape committed Apr 6, 2015
1 parent c297bd8 commit 23be402
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 0 deletions.
18 changes: 18 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,24 @@ Calling the class or definition from outside the current module will fail. For e

*Type*: statement

#### `pw_hash`

Hashes a password using the crypt function. Provides a hash usable on most POSIX systems.

The first argument to this function is the password to hash. If it is undef or an empty string, this function returns undef.

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:

|Hash type |Specifier|
|---------------------|---------|
|MD5 |1 |
|SHA-256 |5 |
|SHA-512 (recommended)|6 |

The third argument to this function is the salt to use.

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.

#### `range`

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].
Expand Down
56 changes: 56 additions & 0 deletions lib/puppet/parser/functions/pw_hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
Puppet::Parser::Functions::newfunction(
:pw_hash,
:type => :rvalue,
:arity => 3,
:doc => "Hashes a password using the crypt function. Provides a hash
usable on most POSIX systems.
The first argument to this function is the password to hash. If it is
undef or an empty string, this function returns undef.
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:
|Hash type |Specifier|
|---------------------|---------|
|MD5 |1 |
|SHA-256 |5 |
|SHA-512 (recommended)|6 |
The third argument to this function is the salt to use.
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.") do |args|
raise ArgumentError, "pw_hash(): wrong number of arguments (#{args.size} for 3)" if args.size != 3
raise ArgumentError, "pw_hash(): first argument must be a string" unless args[0].is_a? String or args[0].nil?
raise ArgumentError, "pw_hash(): second argument must be a string" unless args[1].is_a? String
hashes = { 'md5' => '1',
'sha-256' => '5',
'sha-512' => '6' }
hash_type = hashes[args[1].downcase]
raise ArgumentError, "pw_hash(): #{args[1]} is not a valid hash type" if hash_type.nil?
raise ArgumentError, "pw_hash(): third argument must be a string" unless args[2].is_a? String
raise ArgumentError, "pw_hash(): third argument must not be empty" if args[2].empty?
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/)

password = args[0]
return nil if password.nil? or password.empty?

# handle weak implementations of String#crypt
if 'test'.crypt('$1$1') != '$1$1$Bp8CU9Oujr9SSEw53WV6G.'
# JRuby < 1.7.17
if RUBY_PLATFORM == 'java'
# override String#crypt for password variable
def password.crypt(salt)
# puppetserver bundles Apache Commons Codec
org.apache.commons.codec.digest.Crypt.crypt(self.to_java_bytes, salt)
end
else
# MS Windows and other systems that don't support enhanced salts
raise Puppet::ParseError, 'system does not support enhanced salts'
end
end
password.crypt("$#{hash_type}$#{args[2]}")
end
34 changes: 34 additions & 0 deletions spec/acceptance/pw_hash_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#! /usr/bin/env ruby -S rspec
require 'spec_helper_acceptance'

# Windows and OS X do not have useful implementations of crypt(3)
describe 'pw_hash function', :unless => (UNSUPPORTED_PLATFORMS + ['windows', 'Darwin']).include?(fact('operatingsystem')) do
describe 'success' do
it 'hashes passwords' do
pp = <<-EOS
$o = pw_hash('password', 6, 'salt')
notice(inline_template('pw_hash is <%= @o.inspect %>'))
EOS

apply_manifest(pp, :catch_failures => true) do |r|
expect(r.stdout).to match(/pw_hash is "\$6\$salt\$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy\.g\."/)
end
end

it 'returns nil if no password is provided' do
pp = <<-EOS
$o = pw_hash('', 6, 'salt')
notice(inline_template('pw_hash is <%= @o.inspect %>'))
EOS

apply_manifest(pp, :catch_failures => true) do |r|
expect(r.stdout).to match(/pw_hash is ""/)
end
end
end
describe 'failure' do
it 'handles less than three arguments'
it 'handles more than three arguments'
it 'handles non strings'
end
end
87 changes: 87 additions & 0 deletions spec/functions/pw_hash_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#! /usr/bin/env ruby -S rspec
require 'spec_helper'

describe "the pw_hash function" do
let(:scope) { PuppetlabsSpec::PuppetInternals.scope }

it "should exist" do
expect(Puppet::Parser::Functions.function("pw_hash")).to eq("function_pw_hash")
end

it "should raise an ArgumentError if there are less than 3 arguments" do
expect { scope.function_pw_hash([]) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
expect { scope.function_pw_hash(['password']) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
expect { scope.function_pw_hash(['password', 'sha-512']) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
end

it "should raise an ArgumentError if there are more than 3 arguments" do
expect { scope.function_pw_hash(['password', 'sha-512', 'salt', 5]) }.to( raise_error(ArgumentError, /[Ww]rong number of arguments/) )
end

it "should raise an ArgumentError if the first argument is not a string" do
expect { scope.function_pw_hash([['password'], 'sha-512', 'salt']) }.to( raise_error(ArgumentError, /first argument must be a string/) )
# in Puppet 3, numbers are passed as strings, so we can't test that
end

it "should return nil if the first argument is empty" do
expect(scope.function_pw_hash(['', 'sha-512', 'salt'])).to eq(nil)
end

it "should return nil if the first argument is undef" do
expect(scope.function_pw_hash([nil, 'sha-512', 'salt'])).to eq(nil)
end

it "should raise an ArgumentError if the second argument is an invalid hash type" do
expect { scope.function_pw_hash(['', 'invalid', 'salt']) }.to( raise_error(ArgumentError, /not a valid hash type/) )
end

it "should raise an ArgumentError if the second argument is not a string" do
expect { scope.function_pw_hash(['', [], 'salt']) }.to( raise_error(ArgumentError, /second argument must be a string/) )
end

it "should raise an ArgumentError if the third argument is not a string" do
expect { scope.function_pw_hash(['password', 'sha-512', ['salt']]) }.to( raise_error(ArgumentError, /third argument must be a string/) )
# in Puppet 3, numbers are passed as strings, so we can't test that
end

it "should raise an ArgumentError if the third argument is empty" do
expect { scope.function_pw_hash(['password', 'sha-512', '']) }.to( raise_error(ArgumentError, /third argument must not be empty/) )
end

it "should raise an ArgumentError if the third argument has invalid characters" do
expect { scope.function_pw_hash(['password', 'sha-512', '%']) }.to( raise_error(ArgumentError, /characters in salt must be in the set/) )
end

it "should fail on platforms with weak implementations of String#crypt" do
String.any_instance.expects(:crypt).with('$1$1').returns('$1SoNol0Ye6Xk')
expect { scope.function_pw_hash(['password', 'sha-512', 'salt']) }.to( raise_error(Puppet::ParseError, /system does not support enhanced salts/) )
end

it "should return a hashed password" do
result = scope.function_pw_hash(['password', 'sha-512', 'salt'])
expect(result).to eql('$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.')
end

it "should use the specified salt" do
result = scope.function_pw_hash(['password', 'sha-512', 'salt'])
expect(result).to match('salt')
end

it "should use the specified hash type" do
resultmd5 = scope.function_pw_hash(['password', 'md5', 'salt'])
resultsha256 = scope.function_pw_hash(['password', 'sha-256', 'salt'])
resultsha512 = scope.function_pw_hash(['password', 'sha-512', 'salt'])

expect(resultmd5).to eql('$1$salt$qJH7.N4xYta3aEG/dfqo/0')
expect(resultsha256).to eql('$5$salt$Gcm6FsVtF/Qa77ZKD.iwsJlCVPY0XSMgLJL0Hnww/c1')
expect(resultsha512).to eql('$6$salt$IxDD3jeSOb5eB1CX5LBsqZFVkJdido3OUILO5Ifz5iwMuTS4XMS130MTSuDDl3aCI6WouIL9AjRbLCelDCy.g.')
end

it "should generate a valid hash" do
password_hash = scope.function_pw_hash(['password', 'sha-512', 'salt'])

hash_parts = password_hash.match(%r{\A\$(.*)\$([a-zA-Z0-9./]+)\$([a-zA-Z0-9./]+)\z})

expect(hash_parts).not_to eql(nil)
end
end

0 comments on commit 23be402

Please sign in to comment.