diff --git a/REFERENCE.md b/REFERENCE.md index 40608063b..26979d17c 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -168,6 +168,7 @@ the provided regular expression. last Period). * [`stdlib::ip_in_range`](#stdlibip_in_range): Returns true if the ipaddress is within the given CIDRs * [`str2bool`](#str2bool): This converts a string to a boolean. +* [`str2saltedpbkdf2`](#str2saltedpbkdf2): Convert a string into a salted SHA512 PBKDF2 password hash like requred for OS X / macOS 10.8+ * [`str2saltedsha512`](#str2saltedsha512): This converts a string to a salted-SHA512 password hash (which is used for OS X versions >= 10.7). * [`strftime`](#strftime): This function returns formatted time. @@ -4477,6 +4478,80 @@ See the function new() in Puppet for details what the Boolean data type supports Returns: `Any` This attempt to convert to boolean strings that contain things like: Y,y, 1, T,t, TRUE,true to 'true' and strings that contain things like: 0, F,f, N,n, false, FALSE, no to 'false'. +### str2saltedpbkdf2 + +Type: Ruby 3.x API + +Convert a string into a salted SHA512 PBKDF2 password hash like requred for OS X / macOS 10.8+. +Note, however, that Apple changes what's required periodically and this may not work for the latest +version of macOS. If that is the case you should get a helpful error message when Puppet tries to set +the pasword using the parameters you provide to the user resource. + +#### Examples + +##### Plain text password and salt + +```puppet +$pw_info = str2saltedpbkdf2('Pa55w0rd', 'Using s0m3 s@lt', 50000) +user { 'jdoe': + ensure => present, + iterations => $pw_info['interations'], + password => $pw_info['password_hex'], + salt => $pw_info['salt_hex'], +} +``` + +##### Sensitive password and salt + +```puppet +$pw = Sensitive.new('Pa55w0rd') +$salt = Sensitive.new('Using s0m3 s@lt') +$pw_info = Sensitive.new(str2saltedpbkdf2($pw, $salt, 50000)) +user { 'jdoe': + ensure => present, + iterations => unwrap($pw_info)['interations'], + password => unwrap($pw_info)['password_hex'], + salt => unwrap($pw_info)['salt_hex'], +} +``` + +#### `str2saltedpbkdf2()` + +Convert a string into a salted SHA512 PBKDF2 password hash like requred for OS X / macOS 10.8+. +Note, however, that Apple changes what's required periodically and this may not work for the latest +version of macOS. If that is the case you should get a helpful error message when Puppet tries to set +the pasword using the parameters you provide to the user resource. + +Returns: `Hash` Provides a hash containing the hex version of the password, the hex version of the salt, and iterations. + +##### Examples + +###### Plain text password and salt + +```puppet +$pw_info = str2saltedpbkdf2('Pa55w0rd', 'Using s0m3 s@lt', 50000) +user { 'jdoe': + ensure => present, + iterations => $pw_info['interations'], + password => $pw_info['password_hex'], + salt => $pw_info['salt_hex'], +} +``` + +###### Sensitive password and salt + +```puppet +$pw = Sensitive.new('Pa55w0rd') +$salt = Sensitive.new('Using s0m3 s@lt') +$pw_info = Sensitive.new(str2saltedpbkdf2($pw, $salt, 50000)) +user { 'jdoe': + ensure => present, + iterations => unwrap($pw_info)['interations'], + password => unwrap($pw_info)['password_hex'], + salt => unwrap($pw_info)['salt_hex'], +} +``` + ### str2saltedsha512 Type: Ruby 3.x API diff --git a/lib/puppet/parser/functions/str2saltedpbkdf2.rb b/lib/puppet/parser/functions/str2saltedpbkdf2.rb new file mode 100644 index 000000000..1f0a19376 --- /dev/null +++ b/lib/puppet/parser/functions/str2saltedpbkdf2.rb @@ -0,0 +1,68 @@ +# str2saltedpbkdf2.rb +# Please note: This function is an implementation of a Ruby class and as such may not be entirely UTF8 compatible. To ensure compatibility please use this function with Ruby 2.4.0 or greater - https://bugs.ruby-lang.org/issues/10085. +# +module Puppet::Parser::Functions + newfunction(:str2saltedpbkdf2, :type => :rvalue, :doc => <<-DOC + @summary Convert a string into a salted SHA512 PBKDF2 password hash like requred for OS X / macOS 10.8+ + + Convert a string into a salted SHA512 PBKDF2 password hash like requred for OS X / macOS 10.8+. + Note, however, that Apple changes what's required periodically and this may not work for the latest + version of macOS. If that is the case you should get a helpful error message when Puppet tries to set + the pasword using the parameters you provide to the user resource. + + @example Plain text password and salt + $pw_info = str2saltedpbkdf2('Pa55w0rd', 'Using s0m3 s@lt', 50000) + user { 'jdoe': + ensure => present, + iterations => $pw_info['interations'], + password => $pw_info['password_hex'], + salt => $pw_info['salt_hex'], + } + + @example Sensitive password and salt + $pw = Sensitive.new('Pa55w0rd') + $salt = Sensitive.new('Using s0m3 s@lt') + $pw_info = Sensitive.new(str2saltedpbkdf2($pw, $salt, 50000)) + user { 'jdoe': + ensure => present, + iterations => unwrap($pw_info)['interations'], + password => unwrap($pw_info)['password_hex'], + salt => unwrap($pw_info)['salt_hex'], + } + + @return [Hash] + Provides a hash containing the hex version of the password, the hex version of the salt, and iterations. + DOC + ) do |args| + require 'openssl' + + raise ArgumentError, "str2saltedpbkdf2(): wrong number of arguments (#{args.size} for 3)" if args.size != 3 + + args.map! do |arg| + if (defined? Puppet::Pops::Types::PSensitiveType::Sensitive) && (arg.is_a? Puppet::Pops::Types::PSensitiveType::Sensitive) + arg.unwrap + else + arg + end + end + + raise ArgumentError, 'str2saltedpbkdf2(): first argument must be a string' unless args[0].is_a?(String) + raise ArgumentError, 'str2saltedpbkdf2(): second argument must be a string' unless args[1].is_a?(String) + raise ArgumentError, 'str2saltedpbkdf2(): second argument must be at least 8 bytes long' unless args[1].bytesize >= 8 + raise ArgumentError, 'str2saltedpbkdf2(): third argument must be an integer' unless args[2].is_a?(Integer) + raise ArgumentError, 'str2saltedpbkdf2(): third argument must be between 40,000 and 70,000' unless args[2] > 40_000 && args[2] < 70_000 + + password = args[0] + salt = args[1] + iterations = args[2] + keylen = 128 + digest = OpenSSL::Digest::SHA512.new + hash = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, keylen, digest) + + { + 'password_hex' => hash.unpack('H*').first, + 'salt_hex' => salt.unpack('H*').first, + 'iterations' => iterations, + } + end +end diff --git a/spec/functions/str2saltedpbkdf2_spec.rb b/spec/functions/str2saltedpbkdf2_spec.rb new file mode 100644 index 000000000..48dcdc48e --- /dev/null +++ b/spec/functions/str2saltedpbkdf2_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe 'str2saltedpbkdf2' do + it { is_expected.not_to eq(nil) } + it { is_expected.to run.with_params.and_raise_error(ArgumentError, %r{wrong number of arguments}i) } + it { is_expected.to run.with_params('Pa55w0rd', 2).and_raise_error(ArgumentError, %r{wrong number of arguments}i) } + it { is_expected.to run.with_params(1, 'Using s0m3 s@lt', 50_000).and_raise_error(ArgumentError, %r{first argument must be a string}) } + it { is_expected.to run.with_params('Pa55w0rd', 1, 50_000).and_raise_error(ArgumentError, %r{second argument must be a string}) } + it { is_expected.to run.with_params('Pa55w0rd', 'U', 50_000).and_raise_error(ArgumentError, %r{second argument must be at least 8 bytes long}) } + it { is_expected.to run.with_params('Pa55w0rd', 'Using s0m3 s@lt', '50000').and_raise_error(ArgumentError, %r{third argument must be an integer}) } + it { is_expected.to run.with_params('Pa55w0rd', 'Using s0m3 s@lt', 1).and_raise_error(ArgumentError, %r{third argument must be between 40,000 and 70,000}) } + + context 'when running with "Pa55w0rd", "Using s0m3 s@lt",and "50000" as params' do + # rubocop:disable Metrics/LineLength + it { + is_expected.to run.with_params('Pa55w0rd', 'Using s0m3 s@lt', 50_000) + .and_return('password_hex' => '3577f79f7d2e73df1cf1eecc36da16fffcd3650126d79e797a8b227492d13de4cdd0656933b43118b7361692f755e5b3c1e0536f826d12442400f3467bcc8fb4ac2235d5648b0f1b0906d0712aecd265834319b5a42e98af2ced81597fd78d1ac916f6eff6122c3577bb120a9f534e2a5c9a58c7d1209e3914c967c6a467b594', + 'salt_hex' => '5573696e672073306d332073406c74', + 'iterations' => 50_000) + } + # rubocop:enable Metrics/LineLength + end +end