Reputation: 113
I want to create a Postgres user with the CREATE USER command and an already hashed digest for the password. After much searching, I thought it was only possible with MD5 until I found this link. I've verified that works like so:
CREATE USER test_user WITH LOGIN PASSWORD 'SCRAM-SHA-256$4096:H45+UIZiJUcEXrB9SHlv5Q==$I0mc87UotsrnezRKv9Ijqn/zjWMGPVdy1zHPARAGfVs=:nSjwT9LGDmAsMo+GqbmC2X/9LMgowTQBjUQsl45gZzA=';
I can then log into that user with the password, which the article doesn't necessarily say but it's "postgres". Now that I know it's possible, how using .NET 5 can I generate a scram-sha-256 digest that Postgres 13 will accept? I've seen other Postgres articles using the outdated MD5 hash where the username is concatenated with the password before hashing. Does that need to happen with the new scram-sha-256 as well? I couldn't find much information on this topic anywhere.
Upvotes: 10
Views: 17395
Reputation: 1
Here is an implementation in perl:
PostgreSQL SCRAM-SHA-256 authentication
#!/usr/bin/perl
# Copyright: 2024 - Guido Brugnara <[email protected]>
# Licence: This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
use Crypt::Salt;
use Crypt::KeyDerivation qw(pbkdf2);
use Crypt::Mac::HMAC qw(hmac hmac_b64);
use Crypt::Digest::SHA256 qw(sha256_b64);
use MIME::Base64;
my $salt = Crypt::Salt::salt(16);
my $iterations = 4096;
my $password = <STDIN>;
chomp $password;
my $digest_key = pbkdf2($password, $salt, $iterations, 'SHA256', 32);
my $client_key = hmac('SHA256', $digest_key ,'Client Key');
my $b64_client_key = sha256_b64($client_key);
my $b64_server_key = hmac_b64('SHA256', $digest_key, 'Server Key');
my $b64_salt = encode_base64($salt, '');
print "SCRAM-SHA-256\$$iterations:$b64_salt\$$b64_client_key:$b64_server_key\n";
Upvotes: 0
Reputation: 325
Here is an implementation in C#
public static class PostgresScramHashCalculator
{
public static string CalculateScramShaHash(string password, int iterationCount)
{
var salt = RandomNumberGenerator.GetBytes(16);
var normalizedPw = Encoding.UTF8.GetBytes(password.Normalize(NormalizationForm.FormKC));
var pbkdf2 = new Rfc2898DeriveBytes(normalizedPw, salt, iterationCount, HashAlgorithmName.SHA256);
var digestKeyBytes = pbkdf2.GetBytes(32);
var hmac = new HMACSHA256(digestKeyBytes);
var clientKey = hmac.ComputeHash(Encoding.UTF8.GetBytes("Client Key"));
var serverKey = hmac.ComputeHash(Encoding.UTF8.GetBytes("Server Key"));
byte[] storedKey = SHA256.HashData(clientKey);
var scramHash = $"SCRAM-SHA-256${iterationCount}:{Convert.ToBase64String(salt)}${Convert.ToBase64String(storedKey)}:{Convert.ToBase64String(serverKey)}";
return scramHash;
}
}
Upvotes: 0
Reputation: 4298
Someone built a Go tool to do this:
https://github.com/supercaracal/scram-sha-256
Here's a python 3 port based on that Go project:
from base64 import standard_b64encode
from hashlib import pbkdf2_hmac, sha256
from os import urandom
import hmac
import sys
salt_size = 16
digest_len = 32
iterations = 4096
def b64enc(b: bytes) -> str:
return standard_b64encode(b).decode('utf8')
def pg_scram_sha256(passwd: str) -> str:
salt = urandom(salt_size)
digest_key = pbkdf2_hmac('sha256', passwd.encode('utf8'), salt, iterations,
digest_len)
client_key = hmac.digest(digest_key, 'Client Key'.encode('utf8'), 'sha256')
stored_key = sha256(client_key).digest()
server_key = hmac.digest(digest_key, 'Server Key'.encode('utf8'), 'sha256')
return (
f'SCRAM-SHA-256${iterations}:{b64enc(salt)}'
f'${b64enc(stored_key)}:{b64enc(server_key)}'
)
def print_usage():
print("Usage: provide single password argument to encrypt")
sys.exit(1)
def main():
args = sys.argv[1:]
if args and len(args) > 1:
print_usage()
if args:
passwd = args[0]
else:
passwd = sys.stdin.read().strip()
if not passwd:
print_usage()
print(pg_scram_sha256(passwd))
if __name__ == "__main__":
main()
NOTE: Passing passwords directly on the command line can leak secrets into shell history, make secrets visible in process output to other users on the system, etc. If you are using this tool to generate real password hashes, take care to protect it from such leakage (e.g. write the plain text password to a text file with secure permissions and redirect it to the script's input using python scram256.py < secret-password.txt
). Also take care to use non-reused randomly generated passwords of sufficient length and other best practices that are beyond the scope of this question/answer.
Upvotes: 10
Reputation: 21
I had this same problem, and I ended up delving into the Npgsql code and combining it with Shane's answer to come up with this. It's not exactly battle-hardened, but I did manage to create a role with it, and then subsequently log in with it. I prefer F#, but it's not too hard to convert this back to C#. Just remember that, in F#, the value of the last line of a function is the return value of that function. Also, in F#, ^^^ is the bitwise-xor operator.
let password_hash (password: string) =
let normalized = System.Text.Encoding.UTF8.GetBytes(password.Normalize(System.Text.NormalizationForm.FormKC))
let salt_len = 16
let default_iterations = 4096
let salt = System.Security.Cryptography.RandomNumberGenerator.GetBytes(salt_len)
let mutable salt1 = Array.create (salt.Length + 4) 0uy
let hmac = new System.Security.Cryptography.HMACSHA256(normalized)
System.Buffer.BlockCopy(salt, 0, salt1, 0, salt.Length)
salt1[salt1.Length - 1] <- 1uy
let mutable hi = hmac.ComputeHash(salt1)
let mutable u1 = hi
for _ in 1 .. default_iterations - 1 do
let u2 = hmac.ComputeHash(u1)
for i in 0 .. hi.Length - 1 do
hi[i] <- hi[i] ^^^ u2[i]
u1 <- u2
let client_key = (new System.Security.Cryptography.HMACSHA256(hi)).ComputeHash(System.Text.Encoding.UTF8.GetBytes("Client Key"))
let stored_key = (System.Security.Cryptography.SHA256.Create()).ComputeHash(client_key)
let server_key = (new System.Security.Cryptography.HMACSHA256(hi)).ComputeHash(System.Text.Encoding.UTF8.GetBytes("Server Key"))
let builder = new System.Text.StringBuilder()
builder.Append("'SCRAM-SHA-256$").Append(default_iterations.ToString()).Append(":").Append(System.Convert.ToBase64String(salt)).Append("$")
.Append(System.Convert.ToBase64String(stored_key)).Append(":").Append(System.Convert.ToBase64String(server_key)).Append("'").ToString()
Upvotes: 2
Reputation: 487
If your intent is to generate a SCRAM-SHA-256 password before you have an operational database, then I found out you can use this method to generate a password hash using Docker tooling.
docker run --rm -it --name postgres-dummy -d -e POSTGRES_HOST_AUTH_METHOD=trust postgres:14-alpine
docker exec -it postgres-dummy psql -U postgres
\password
(type in your password twice)
select rolpassword from pg_authid where rolname = 'postgres';
\q
docker stop postgres-dummy
I know it's not a way to do it in .NET, but hopefully it is useful for someone.
Upvotes: 5
Reputation: 2826
I don't know how to generate a scram-sha-256 digest with .NET 5.
However, if the scram-sha-256 digest is all you need you can use a workaround by creating a dummy postgres user locally and echoing the encryped password with the createuser
command.
For example (usually you run this as Linux postgres user: su postgres
):
$ createuser dummyuser -e --pwprompt
Enter password for new role:
Enter it again:
SELECT pg_catalog.set_config('search_path', '', false);
CREATE ROLE dummyuser PASSWORD 'SCRAM-SHA-256$4096:VnimR0aOywxZzY82nzy9Fg==$qF9uMCU6YsKoecvRjP8jSmZZxrXgn5VwzhHwfoWo5Xg=:xGYfBUvGsu9mZFiq1nSFaHi7uN8n47IDwHO32IeK9io=' NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN;
You can now just copy the digest into your query. Of course that only works if you don't need to generate the digest dynamically with .NET.
Don't forget to drop the dummy user:
$ dropuser dummyuser
Also, in case your local postgres db still uses/generates md5 you have to change that to scram by using following query, ran as postgres superuser:
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
SELECT pg_reload_conf();
If you, however, want to create the digest dynamically with .NET, I recommend you to have a look at the source code of the createuser
command...
Update one day later:
Here is the specific source code of the createuser
command that encrypts the password into a scram-sha-256 string:
To make life easier, here are links to the functions (probably not all) the above function is calling
You should be able to rewrite that code in .NET or any other language. Hope it helps!
Upvotes: 8