diff --git a/lib/puppet/functions/postgresql/postgresql_password.rb b/lib/puppet/functions/postgresql/postgresql_password.rb index f22db18f05..89de4b9d9e 100644 --- a/lib/puppet/functions/postgresql/postgresql_password.rb +++ b/lib/puppet/functions/postgresql/postgresql_password.rb @@ -1,4 +1,6 @@ # frozen_string_literal: true +require 'openssl' +require 'base64' # @summary This function returns the postgresql password hash from the clear text username / password Puppet::Functions.create_function(:'postgresql::postgresql_password') do @@ -8,6 +10,10 @@ # The clear text `password` # @param sensitive # If the Postgresql-Passwordhash should be of Datatype Sensitive[String] + # @param hash + # Set type for password hash + # @param salt + # Use a specific salt value for scram-sha-256, default is username # # @return # The postgresql password hash from the clear text username / password. @@ -15,16 +21,55 @@ required_param 'Variant[String[1], Integer]', :username required_param 'Variant[String[1], Sensitive[String[1]], Integer]', :password optional_param 'Boolean', :sensitive + optional_param "Optional[Enum['md5', 'scram-sha-256']]", :hash + optional_param 'Optional[Variant[String[1], Integer]]', :salt return_type 'Variant[String, Sensitive[String]]' end - def default_impl(username, password, sensitive = false) + def default_impl(username, password, sensitive = false, hash = 'md5', salt = nil) password = password.unwrap if password.respond_to?(:unwrap) - result_string = 'md5' + Digest::MD5.hexdigest(password.to_s + username.to_s) + pass = if hash == 'md5' + 'md5' + Digest::MD5.hexdigest(password.to_s + username.to_s) + else + pg_sha256(password, (salt || username)) + end if sensitive - Puppet::Pops::Types::PSensitiveType::Sensitive.new(result_string) + Puppet::Pops::Types::PSensitiveType::Sensitive.new(pass) else - result_string + pass end end + + def pg_sha256(password, salt) + digest = digest_key(password, salt) + 'SCRAM-SHA-256$%s:%s$%s:%s' % [ + '4096', + Base64.strict_encode64(salt), + Base64.strict_encode64(client_key(digest)), + Base64.strict_encode64(server_key(digest)) + ] + end + + def digest_key(password, salt) + OpenSSL::KDF.pbkdf2_hmac( + password, + salt: salt, + iterations: 4096, + length: 32, + hash: OpenSSL::Digest::SHA256.new + ) + end + + def client_key(digest_key) + hmac = OpenSSL::HMAC.new(digest_key, OpenSSL::Digest::SHA256.new) + hmac << 'Client Key' + hmac.digest + OpenSSL::Digest.new('SHA256').digest hmac.digest + end + + def server_key(digest_key) + hmac = OpenSSL::HMAC.new(digest_key, OpenSSL::Digest::SHA256.new) + hmac << 'Server Key' + hmac.digest + end end diff --git a/manifests/server/role.pp b/manifests/server/role.pp index cba36ec54c..00edc75bd5 100644 --- a/manifests/server/role.pp +++ b/manifests/server/role.pp @@ -18,6 +18,8 @@ # @param psql_group Sets the OS group to run psql # @param psql_path Sets path to psql command # @param module_workdir Specifies working directory under which the psql command should be executed. May need to specify if '/tmp' is on volume mounted with noexec option. +# @param hash Specify the hash method for pg password +# @param salt Specify the salt use for the scram-sha-256 encoding password (default username) define postgresql::server::role ( $update_password = true, Variant[Boolean, String, Sensitive[String]] $password_hash = false, @@ -37,6 +39,8 @@ $psql_path = $postgresql::server::psql_path, $module_workdir = $postgresql::server::module_workdir, Enum['present', 'absent'] $ensure = 'present', + Enum['md5', 'scram-sha-256'] $hash = 'md5', + Optional[Variant[String[1], Integer]] $salt = undef, ) { $password_hash_unsensitive = if $password_hash =~ Sensitive[String] { $password_hash.unwrap @@ -130,14 +134,19 @@ } if $password_hash_unsensitive and $update_password { - if($password_hash_unsensitive =~ /^md5.+/) { + if($password_hash_unsensitive =~ /^(md5|SCRAM-SHA-256).+/) { $pwd_hash_sql = $password_hash_unsensitive } else { - $pwd_md5 = md5("${password_hash_unsensitive}${username}") - $pwd_hash_sql = "md5${pwd_md5}" + $pwd_hash_sql = postgresql::postgresql_password( + $username, + $password_hash, + $password_hash =~ Sensitive[String], + $hash, + $salt, + ) } postgresql_psql { "ALTER ROLE ${username} ENCRYPTED PASSWORD ****": - command => Sensitive("ALTER ROLE \"${username}\" ${password_sql}"), + command => Sensitive("ALTER ROLE \"${username}\" ENCRYPTED PASSWORD '${pwd_hash_sql}'"), unless => Sensitive("SELECT 1 FROM pg_shadow WHERE usename = '${username}' AND passwd = '${pwd_hash_sql}'"), sensitive => true, } diff --git a/spec/spec_helper_local.rb b/spec/spec_helper_local.rb index 0137edd85b..5d9b349a80 100644 --- a/spec/spec_helper_local.rb +++ b/spec/spec_helper_local.rb @@ -48,10 +48,24 @@ def param(type, title, param) it { is_expected.not_to eq(nil) } it { - is_expected.to run.with_params('foo', 'bar').and_return('md596948aad3fcae80c08a35c9b5958cd89') + is_expected.to run.with_params('foo', 'bar').and_return( + 'md596948aad3fcae80c08a35c9b5958cd89' + ) } it { - is_expected.to run.with_params('foo', 1234).and_return('md539a0e1b308278a8de5e007cd1f795920') + is_expected.to run.with_params('foo', 1234).and_return( + 'md539a0e1b308278a8de5e007cd1f795920' + ) + } + it { + is_expected.to run.with_params('foo', 'bar', nil, 'scram-sha-256').and_return( + 'SCRAM-SHA-256$4096:YmFy$y1VOaTvvs4V3OECvMzre9FtgCZClGuBLVE6sNPsTKbs=:HwFqmSKbihSyHMqkhufOy++cWCFIoTRSg8y6YgeALzE=' + ) + } + it { + is_expected.to run.with_params('foo', 'bar', nil, 'scram-sha-256', 'salt').and_return( + 'SCRAM-SHA-256$4096:c2FsdA==$zOt2zFfUQMbpQf3/vRnYB33QDK/L7APOBHniLy39j/4=:DcW5Jp8Do7wYhVp1f9aT0cyhUfzIAozGcvzXZj+M3YI=' + ) } it 'raises an error if there is only 1 argument' do is_expected.to run.with_params('foo').and_raise_error(StandardError)