LCOV - code coverage report
Current view: top level - src/backend/libpq - crypt.c (source / functions) Hit Total Coverage
Test: PostgreSQL 18devel Lines: 68 87 78.2 %
Date: 2025-01-18 04:15:08 Functions: 5 5 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /*-------------------------------------------------------------------------
       2             :  *
       3             :  * crypt.c
       4             :  *    Functions for dealing with encrypted passwords stored in
       5             :  *    pg_authid.rolpassword.
       6             :  *
       7             :  * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
       8             :  * Portions Copyright (c) 1994, Regents of the University of California
       9             :  *
      10             :  * src/backend/libpq/crypt.c
      11             :  *
      12             :  *-------------------------------------------------------------------------
      13             :  */
      14             : #include "postgres.h"
      15             : 
      16             : #include <unistd.h>
      17             : 
      18             : #include "catalog/pg_authid.h"
      19             : #include "common/md5.h"
      20             : #include "common/scram-common.h"
      21             : #include "libpq/crypt.h"
      22             : #include "libpq/scram.h"
      23             : #include "utils/builtins.h"
      24             : #include "utils/syscache.h"
      25             : #include "utils/timestamp.h"
      26             : 
      27             : /* Enables deprecation warnings for MD5 passwords. */
      28             : bool        md5_password_warnings = true;
      29             : 
      30             : /*
      31             :  * Fetch stored password for a user, for authentication.
      32             :  *
      33             :  * On error, returns NULL, and stores a palloc'd string describing the reason,
      34             :  * for the postmaster log, in *logdetail.  The error reason should *not* be
      35             :  * sent to the client, to avoid giving away user information!
      36             :  */
      37             : char *
      38         144 : get_role_password(const char *role, const char **logdetail)
      39             : {
      40         144 :     TimestampTz vuntil = 0;
      41             :     HeapTuple   roleTup;
      42             :     Datum       datum;
      43             :     bool        isnull;
      44             :     char       *shadow_pass;
      45             : 
      46             :     /* Get role info from pg_authid */
      47         144 :     roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
      48         144 :     if (!HeapTupleIsValid(roleTup))
      49             :     {
      50           0 :         *logdetail = psprintf(_("Role \"%s\" does not exist."),
      51             :                               role);
      52           0 :         return NULL;            /* no such user */
      53             :     }
      54             : 
      55         144 :     datum = SysCacheGetAttr(AUTHNAME, roleTup,
      56             :                             Anum_pg_authid_rolpassword, &isnull);
      57         144 :     if (isnull)
      58             :     {
      59           0 :         ReleaseSysCache(roleTup);
      60           0 :         *logdetail = psprintf(_("User \"%s\" has no password assigned."),
      61             :                               role);
      62           0 :         return NULL;            /* user has no password */
      63             :     }
      64         144 :     shadow_pass = TextDatumGetCString(datum);
      65             : 
      66         144 :     datum = SysCacheGetAttr(AUTHNAME, roleTup,
      67             :                             Anum_pg_authid_rolvaliduntil, &isnull);
      68         144 :     if (!isnull)
      69           0 :         vuntil = DatumGetTimestampTz(datum);
      70             : 
      71         144 :     ReleaseSysCache(roleTup);
      72             : 
      73             :     /*
      74             :      * Password OK, but check to be sure we are not past rolvaliduntil
      75             :      */
      76         144 :     if (!isnull && vuntil < GetCurrentTimestamp())
      77             :     {
      78           0 :         *logdetail = psprintf(_("User \"%s\" has an expired password."),
      79             :                               role);
      80           0 :         return NULL;
      81             :     }
      82             : 
      83         144 :     return shadow_pass;
      84             : }
      85             : 
      86             : /*
      87             :  * What kind of a password type is 'shadow_pass'?
      88             :  */
      89             : PasswordType
      90         758 : get_password_type(const char *shadow_pass)
      91             : {
      92             :     char       *encoded_salt;
      93             :     int         iterations;
      94         758 :     int         key_length = 0;
      95             :     pg_cryptohash_type hash_type;
      96             :     uint8       stored_key[SCRAM_MAX_KEY_LEN];
      97             :     uint8       server_key[SCRAM_MAX_KEY_LEN];
      98             : 
      99         758 :     if (strncmp(shadow_pass, "md5", 3) == 0 &&
     100         120 :         strlen(shadow_pass) == MD5_PASSWD_LEN &&
     101         108 :         strspn(shadow_pass + 3, MD5_PASSWD_CHARSET) == MD5_PASSWD_LEN - 3)
     102          96 :         return PASSWORD_TYPE_MD5;
     103         662 :     if (parse_scram_secret(shadow_pass, &iterations, &hash_type, &key_length,
     104             :                            &encoded_salt, stored_key, server_key))
     105         404 :         return PASSWORD_TYPE_SCRAM_SHA_256;
     106         258 :     return PASSWORD_TYPE_PLAINTEXT;
     107             : }
     108             : 
     109             : /*
     110             :  * Given a user-supplied password, convert it into a secret of
     111             :  * 'target_type' kind.
     112             :  *
     113             :  * If the password is already in encrypted form, we cannot reverse the
     114             :  * hash, so it is stored as it is regardless of the requested type.
     115             :  */
     116             : char *
     117         166 : encrypt_password(PasswordType target_type, const char *role,
     118             :                  const char *password)
     119             : {
     120         166 :     PasswordType guessed_type = get_password_type(password);
     121         166 :     char       *encrypted_password = NULL;
     122         166 :     const char *errstr = NULL;
     123             : 
     124         166 :     if (guessed_type != PASSWORD_TYPE_PLAINTEXT)
     125             :     {
     126             :         /*
     127             :          * Cannot convert an already-encrypted password from one format to
     128             :          * another, so return it as it is.
     129             :          */
     130          42 :         encrypted_password = pstrdup(password);
     131             :     }
     132             :     else
     133             :     {
     134         124 :         switch (target_type)
     135             :         {
     136          22 :             case PASSWORD_TYPE_MD5:
     137          22 :                 encrypted_password = palloc(MD5_PASSWD_LEN + 1);
     138             : 
     139          22 :                 if (!pg_md5_encrypt(password, role, strlen(role),
     140             :                                     encrypted_password, &errstr))
     141           0 :                     elog(ERROR, "password encryption failed: %s", errstr);
     142          22 :                 break;
     143             : 
     144         102 :             case PASSWORD_TYPE_SCRAM_SHA_256:
     145         102 :                 encrypted_password = pg_be_scram_build_secret(password);
     146         102 :                 break;
     147             : 
     148           0 :             case PASSWORD_TYPE_PLAINTEXT:
     149           0 :                 elog(ERROR, "cannot encrypt password with 'plaintext'");
     150             :                 break;
     151             :         }
     152         166 :     }
     153             : 
     154             :     Assert(encrypted_password);
     155             : 
     156             :     /*
     157             :      * Valid password hashes may be very long, but we don't want to store
     158             :      * anything that might need out-of-line storage, since de-TOASTing won't
     159             :      * work during authentication because we haven't selected a database yet
     160             :      * and cannot read pg_class. 512 bytes should be more than enough for all
     161             :      * practical use, so fail for anything longer.
     162             :      */
     163         166 :     if (encrypted_password &&   /* keep compiler quiet */
     164         166 :         strlen(encrypted_password) > MAX_ENCRYPTED_PASSWORD_LEN)
     165             :     {
     166             :         /*
     167             :          * We don't expect any of our own hashing routines to produce hashes
     168             :          * that are too long.
     169             :          */
     170             :         Assert(guessed_type != PASSWORD_TYPE_PLAINTEXT);
     171             : 
     172          12 :         ereport(ERROR,
     173             :                 (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
     174             :                  errmsg("encrypted password is too long"),
     175             :                  errdetail("Encrypted passwords must be no longer than %d bytes.",
     176             :                            MAX_ENCRYPTED_PASSWORD_LEN)));
     177             :     }
     178             : 
     179         302 :     if (md5_password_warnings &&
     180         148 :         get_password_type(encrypted_password) == PASSWORD_TYPE_MD5)
     181          34 :         ereport(WARNING,
     182             :                 (errcode(ERRCODE_WARNING_DEPRECATED_FEATURE),
     183             :                  errmsg("setting an MD5-encrypted password"),
     184             :                  errdetail("MD5 password support is deprecated and will be removed in a future release of PostgreSQL."),
     185             :                  errhint("Refer to the PostgreSQL documentation for details about migrating to another password type.")));
     186             : 
     187         154 :     return encrypted_password;
     188             : }
     189             : 
     190             : /*
     191             :  * Check MD5 authentication response, and return STATUS_OK or STATUS_ERROR.
     192             :  *
     193             :  * 'shadow_pass' is the user's correct password or password hash, as stored
     194             :  * in pg_authid.rolpassword.
     195             :  * 'client_pass' is the response given by the remote user to the MD5 challenge.
     196             :  * 'md5_salt' is the salt used in the MD5 authentication challenge.
     197             :  *
     198             :  * In the error case, save a string at *logdetail that will be sent to the
     199             :  * postmaster log (but not the client).
     200             :  */
     201             : int
     202           2 : md5_crypt_verify(const char *role, const char *shadow_pass,
     203             :                  const char *client_pass,
     204             :                  const char *md5_salt, int md5_salt_len,
     205             :                  const char **logdetail)
     206             : {
     207             :     int         retval;
     208             :     char        crypt_pwd[MD5_PASSWD_LEN + 1];
     209           2 :     const char *errstr = NULL;
     210             : 
     211             :     Assert(md5_salt_len > 0);
     212             : 
     213           2 :     if (get_password_type(shadow_pass) != PASSWORD_TYPE_MD5)
     214             :     {
     215             :         /* incompatible password hash format. */
     216           0 :         *logdetail = psprintf(_("User \"%s\" has a password that cannot be used with MD5 authentication."),
     217             :                               role);
     218           0 :         return STATUS_ERROR;
     219             :     }
     220             : 
     221             :     /*
     222             :      * Compute the correct answer for the MD5 challenge.
     223             :      */
     224             :     /* stored password already encrypted, only do salt */
     225           2 :     if (!pg_md5_encrypt(shadow_pass + strlen("md5"),
     226             :                         md5_salt, md5_salt_len,
     227             :                         crypt_pwd, &errstr))
     228             :     {
     229           0 :         *logdetail = errstr;
     230           0 :         return STATUS_ERROR;
     231             :     }
     232             : 
     233           2 :     if (strcmp(client_pass, crypt_pwd) == 0)
     234           2 :         retval = STATUS_OK;
     235             :     else
     236             :     {
     237           0 :         *logdetail = psprintf(_("Password does not match for user \"%s\"."),
     238             :                               role);
     239           0 :         retval = STATUS_ERROR;
     240             :     }
     241             : 
     242           2 :     return retval;
     243             : }
     244             : 
     245             : /*
     246             :  * Check given password for given user, and return STATUS_OK or STATUS_ERROR.
     247             :  *
     248             :  * 'shadow_pass' is the user's correct password hash, as stored in
     249             :  * pg_authid.rolpassword.
     250             :  * 'client_pass' is the password given by the remote user.
     251             :  *
     252             :  * In the error case, store a string at *logdetail that will be sent to the
     253             :  * postmaster log (but not the client).
     254             :  */
     255             : int
     256         202 : plain_crypt_verify(const char *role, const char *shadow_pass,
     257             :                    const char *client_pass,
     258             :                    const char **logdetail)
     259             : {
     260             :     char        crypt_client_pass[MD5_PASSWD_LEN + 1];
     261         202 :     const char *errstr = NULL;
     262             : 
     263             :     /*
     264             :      * Client sent password in plaintext.  If we have an MD5 hash stored, hash
     265             :      * the password the client sent, and compare the hashes.  Otherwise
     266             :      * compare the plaintext passwords directly.
     267             :      */
     268         202 :     switch (get_password_type(shadow_pass))
     269             :     {
     270          52 :         case PASSWORD_TYPE_SCRAM_SHA_256:
     271          52 :             if (scram_verify_plain_password(role,
     272             :                                             client_pass,
     273             :                                             shadow_pass))
     274             :             {
     275          24 :                 return STATUS_OK;
     276             :             }
     277             :             else
     278             :             {
     279          28 :                 *logdetail = psprintf(_("Password does not match for user \"%s\"."),
     280             :                                       role);
     281          28 :                 return STATUS_ERROR;
     282             :             }
     283             :             break;
     284             : 
     285          26 :         case PASSWORD_TYPE_MD5:
     286          26 :             if (!pg_md5_encrypt(client_pass,
     287             :                                 role,
     288             :                                 strlen(role),
     289             :                                 crypt_client_pass,
     290             :                                 &errstr))
     291             :             {
     292           0 :                 *logdetail = errstr;
     293           0 :                 return STATUS_ERROR;
     294             :             }
     295          26 :             if (strcmp(crypt_client_pass, shadow_pass) == 0)
     296          10 :                 return STATUS_OK;
     297             :             else
     298             :             {
     299          16 :                 *logdetail = psprintf(_("Password does not match for user \"%s\"."),
     300             :                                       role);
     301          16 :                 return STATUS_ERROR;
     302             :             }
     303             :             break;
     304             : 
     305         124 :         case PASSWORD_TYPE_PLAINTEXT:
     306             : 
     307             :             /*
     308             :              * We never store passwords in plaintext, so this shouldn't
     309             :              * happen.
     310             :              */
     311         124 :             break;
     312             :     }
     313             : 
     314             :     /*
     315             :      * This shouldn't happen.  Plain "password" authentication is possible
     316             :      * with any kind of stored password hash.
     317             :      */
     318         124 :     *logdetail = psprintf(_("Password of user \"%s\" is in unrecognized format."),
     319             :                           role);
     320         124 :     return STATUS_ERROR;
     321             : }

Generated by: LCOV version 1.14