LCOV - code coverage report
Current view: top level - src/backend/libpq - crypt.c (source / functions) Coverage Total Hit
Test: PostgreSQL 19devel Lines: 80.9 % 110 89
Test Date: 2026-03-13 06:15:19 Functions: 100.0 % 5 5
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-2026, 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 "miscadmin.h"
      24              : #include "utils/builtins.h"
      25              : #include "utils/memutils.h"
      26              : #include "utils/syscache.h"
      27              : #include "utils/timestamp.h"
      28              : 
      29              : /* Threshold for password expiration warnings. */
      30              : int         password_expiration_warning_threshold = 604800;
      31              : 
      32              : /* Enables deprecation warnings for MD5 passwords. */
      33              : bool        md5_password_warnings = true;
      34              : 
      35              : /*
      36              :  * Fetch stored password for a user, for authentication.
      37              :  *
      38              :  * On error, returns NULL, and stores a palloc'd string describing the reason,
      39              :  * for the postmaster log, in *logdetail.  The error reason should *not* be
      40              :  * sent to the client, to avoid giving away user information!
      41              :  */
      42              : char *
      43           84 : get_role_password(const char *role, const char **logdetail)
      44              : {
      45           84 :     TimestampTz vuntil = 0;
      46              :     HeapTuple   roleTup;
      47              :     Datum       datum;
      48              :     bool        isnull;
      49              :     char       *shadow_pass;
      50              : 
      51              :     /* Get role info from pg_authid */
      52           84 :     roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
      53           84 :     if (!HeapTupleIsValid(roleTup))
      54              :     {
      55            0 :         *logdetail = psprintf(_("Role \"%s\" does not exist."),
      56              :                               role);
      57            0 :         return NULL;            /* no such user */
      58              :     }
      59              : 
      60           84 :     datum = SysCacheGetAttr(AUTHNAME, roleTup,
      61              :                             Anum_pg_authid_rolpassword, &isnull);
      62           84 :     if (isnull)
      63              :     {
      64            0 :         ReleaseSysCache(roleTup);
      65            0 :         *logdetail = psprintf(_("User \"%s\" has no password assigned."),
      66              :                               role);
      67            0 :         return NULL;            /* user has no password */
      68              :     }
      69           84 :     shadow_pass = TextDatumGetCString(datum);
      70              : 
      71           84 :     datum = SysCacheGetAttr(AUTHNAME, roleTup,
      72              :                             Anum_pg_authid_rolvaliduntil, &isnull);
      73           84 :     if (!isnull)
      74            3 :         vuntil = DatumGetTimestampTz(datum);
      75              : 
      76           84 :     ReleaseSysCache(roleTup);
      77              : 
      78              :     /*
      79              :      * Password OK, but check to be sure we are not past rolvaliduntil or
      80              :      * password_expiration_warning_threshold.
      81              :      */
      82           84 :     if (!isnull)
      83              :     {
      84            3 :         TimestampTz now = GetCurrentTimestamp();
      85            3 :         uint64      expire_time = TimestampDifferenceMicroseconds(now, vuntil);
      86              : 
      87              :         /*
      88              :          * If we're past rolvaliduntil, the connection attempt should fail, so
      89              :          * update logdetail and return NULL.
      90              :          */
      91            3 :         if (vuntil < now)
      92              :         {
      93            1 :             *logdetail = psprintf(_("User \"%s\" has an expired password."),
      94              :                                   role);
      95            1 :             return NULL;
      96              :         }
      97              : 
      98              :         /*
      99              :          * If we're past the warning threshold, the connection attempt should
     100              :          * succeed, but we still want to emit a warning.  To do so, we queue
     101              :          * the warning message using StoreConnectionWarning() so that it will
     102              :          * be emitted at the end of InitPostgres(), and we return normally.
     103              :          */
     104            2 :         if (expire_time / USECS_PER_SEC < password_expiration_warning_threshold)
     105              :         {
     106              :             MemoryContext oldcontext;
     107              :             int         days;
     108              :             int         hours;
     109              :             int         minutes;
     110              :             char       *warning;
     111              :             char       *detail;
     112              : 
     113            1 :             oldcontext = MemoryContextSwitchTo(TopMemoryContext);
     114              : 
     115            1 :             days = expire_time / USECS_PER_DAY;
     116            1 :             hours = (expire_time % USECS_PER_DAY) / USECS_PER_HOUR;
     117            1 :             minutes = (expire_time % USECS_PER_HOUR) / USECS_PER_MINUTE;
     118              : 
     119            1 :             warning = pstrdup(_("role password will expire soon"));
     120              : 
     121            1 :             if (days > 0)
     122            1 :                 detail = psprintf(ngettext("The password for role \"%s\" will expire in %d day.",
     123              :                                            "The password for role \"%s\" will expire in %d days.",
     124              :                                            days),
     125              :                                   role, days);
     126            0 :             else if (hours > 0)
     127            0 :                 detail = psprintf(ngettext("The password for role \"%s\" will expire in %d hour.",
     128              :                                            "The password for role \"%s\" will expire in %d hours.",
     129              :                                            hours),
     130              :                                   role, hours);
     131            0 :             else if (minutes > 0)
     132            0 :                 detail = psprintf(ngettext("The password for role \"%s\" will expire in %d minute.",
     133              :                                            "The password for role \"%s\" will expire in %d minutes.",
     134              :                                            minutes),
     135              :                                   role, minutes);
     136              :             else
     137            0 :                 detail = psprintf(_("The password for role \"%s\" will expire in less than 1 minute."),
     138              :                                   role);
     139              : 
     140            1 :             StoreConnectionWarning(warning, detail);
     141              : 
     142            1 :             MemoryContextSwitchTo(oldcontext);
     143              :         }
     144              :     }
     145              : 
     146           83 :     return shadow_pass;
     147              : }
     148              : 
     149              : /*
     150              :  * What kind of a password type is 'shadow_pass'?
     151              :  */
     152              : PasswordType
     153          422 : get_password_type(const char *shadow_pass)
     154              : {
     155              :     char       *encoded_salt;
     156              :     int         iterations;
     157          422 :     int         key_length = 0;
     158              :     pg_cryptohash_type hash_type;
     159              :     uint8       stored_key[SCRAM_MAX_KEY_LEN];
     160              :     uint8       server_key[SCRAM_MAX_KEY_LEN];
     161              : 
     162          422 :     if (strncmp(shadow_pass, "md5", 3) == 0 &&
     163           60 :         strlen(shadow_pass) == MD5_PASSWD_LEN &&
     164           54 :         strspn(shadow_pass + 3, MD5_PASSWD_CHARSET) == MD5_PASSWD_LEN - 3)
     165           48 :         return PASSWORD_TYPE_MD5;
     166          374 :     if (parse_scram_secret(shadow_pass, &iterations, &hash_type, &key_length,
     167              :                            &encoded_salt, stored_key, server_key))
     168          233 :         return PASSWORD_TYPE_SCRAM_SHA_256;
     169          141 :     return PASSWORD_TYPE_PLAINTEXT;
     170              : }
     171              : 
     172              : /*
     173              :  * Given a user-supplied password, convert it into a secret of
     174              :  * 'target_type' kind.
     175              :  *
     176              :  * If the password is already in encrypted form, we cannot reverse the
     177              :  * hash, so it is stored as it is regardless of the requested type.
     178              :  */
     179              : char *
     180           90 : encrypt_password(PasswordType target_type, const char *role,
     181              :                  const char *password)
     182              : {
     183           90 :     PasswordType guessed_type = get_password_type(password);
     184           90 :     char       *encrypted_password = NULL;
     185           90 :     const char *errstr = NULL;
     186              : 
     187           90 :     if (guessed_type != PASSWORD_TYPE_PLAINTEXT)
     188              :     {
     189              :         /*
     190              :          * Cannot convert an already-encrypted password from one format to
     191              :          * another, so return it as it is.
     192              :          */
     193           22 :         encrypted_password = pstrdup(password);
     194              :     }
     195              :     else
     196              :     {
     197           68 :         switch (target_type)
     198              :         {
     199           11 :             case PASSWORD_TYPE_MD5:
     200           11 :                 encrypted_password = palloc(MD5_PASSWD_LEN + 1);
     201              : 
     202           11 :                 if (!pg_md5_encrypt(password, (const uint8 *) role, strlen(role),
     203              :                                     encrypted_password, &errstr))
     204            0 :                     elog(ERROR, "password encryption failed: %s", errstr);
     205           11 :                 break;
     206              : 
     207           57 :             case PASSWORD_TYPE_SCRAM_SHA_256:
     208           57 :                 encrypted_password = pg_be_scram_build_secret(password);
     209           57 :                 break;
     210              : 
     211            0 :             case PASSWORD_TYPE_PLAINTEXT:
     212            0 :                 elog(ERROR, "cannot encrypt password with 'plaintext'");
     213              :                 break;
     214              :         }
     215              :     }
     216              : 
     217              :     Assert(encrypted_password);
     218              : 
     219              :     /*
     220              :      * Valid password hashes may be very long, but we don't want to store
     221              :      * anything that might need out-of-line storage, since de-TOASTing won't
     222              :      * work during authentication because we haven't selected a database yet
     223              :      * and cannot read pg_class. 512 bytes should be more than enough for all
     224              :      * practical use, so fail for anything longer.
     225              :      */
     226           90 :     if (encrypted_password &&   /* keep compiler quiet */
     227           90 :         strlen(encrypted_password) > MAX_ENCRYPTED_PASSWORD_LEN)
     228              :     {
     229              :         /*
     230              :          * We don't expect any of our own hashing routines to produce hashes
     231              :          * that are too long.
     232              :          */
     233              :         Assert(guessed_type != PASSWORD_TYPE_PLAINTEXT);
     234              : 
     235            6 :         ereport(ERROR,
     236              :                 (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
     237              :                  errmsg("encrypted password is too long"),
     238              :                  errdetail("Encrypted passwords must be no longer than %d bytes.",
     239              :                            MAX_ENCRYPTED_PASSWORD_LEN)));
     240              :     }
     241              : 
     242          165 :     if (md5_password_warnings &&
     243           81 :         get_password_type(encrypted_password) == PASSWORD_TYPE_MD5)
     244           17 :         ereport(WARNING,
     245              :                 (errcode(ERRCODE_WARNING_DEPRECATED_FEATURE),
     246              :                  errmsg("setting an MD5-encrypted password"),
     247              :                  errdetail("MD5 password support is deprecated and will be removed in a future release of PostgreSQL."),
     248              :                  errhint("Refer to the PostgreSQL documentation for details about migrating to another password type.")));
     249              : 
     250           84 :     return encrypted_password;
     251              : }
     252              : 
     253              : /*
     254              :  * Check MD5 authentication response, and return STATUS_OK or STATUS_ERROR.
     255              :  *
     256              :  * 'shadow_pass' is the user's correct password or password hash, as stored
     257              :  * in pg_authid.rolpassword.
     258              :  * 'client_pass' is the response given by the remote user to the MD5 challenge.
     259              :  * 'md5_salt' is the salt used in the MD5 authentication challenge.
     260              :  *
     261              :  * In the error case, save a string at *logdetail that will be sent to the
     262              :  * postmaster log (but not the client).
     263              :  */
     264              : int
     265            1 : md5_crypt_verify(const char *role, const char *shadow_pass,
     266              :                  const char *client_pass,
     267              :                  const uint8 *md5_salt, int md5_salt_len,
     268              :                  const char **logdetail)
     269              : {
     270              :     int         retval;
     271              :     char        crypt_pwd[MD5_PASSWD_LEN + 1];
     272            1 :     const char *errstr = NULL;
     273              : 
     274              :     Assert(md5_salt_len > 0);
     275              : 
     276            1 :     if (get_password_type(shadow_pass) != PASSWORD_TYPE_MD5)
     277              :     {
     278              :         /* incompatible password hash format. */
     279            0 :         *logdetail = psprintf(_("User \"%s\" has a password that cannot be used with MD5 authentication."),
     280              :                               role);
     281            0 :         return STATUS_ERROR;
     282              :     }
     283              : 
     284              :     /*
     285              :      * Compute the correct answer for the MD5 challenge.
     286              :      */
     287              :     /* stored password already encrypted, only do salt */
     288            1 :     if (!pg_md5_encrypt(shadow_pass + strlen("md5"),
     289              :                         md5_salt, md5_salt_len,
     290              :                         crypt_pwd, &errstr))
     291              :     {
     292            0 :         *logdetail = errstr;
     293            0 :         return STATUS_ERROR;
     294              :     }
     295              : 
     296            1 :     if (strcmp(client_pass, crypt_pwd) == 0)
     297              :     {
     298            1 :         retval = STATUS_OK;
     299              : 
     300            1 :         if (md5_password_warnings)
     301              :         {
     302              :             MemoryContext oldcontext;
     303              :             char       *warning;
     304              :             char       *detail;
     305              : 
     306            1 :             oldcontext = MemoryContextSwitchTo(TopMemoryContext);
     307              : 
     308            1 :             warning = pstrdup(_("authenticated with an MD5-encrypted password"));
     309            1 :             detail = pstrdup(_("MD5 password support is deprecated and will be removed in a future release of PostgreSQL."));
     310            1 :             StoreConnectionWarning(warning, detail);
     311              : 
     312            1 :             MemoryContextSwitchTo(oldcontext);
     313              :         }
     314              :     }
     315              :     else
     316              :     {
     317            0 :         *logdetail = psprintf(_("Password does not match for user \"%s\"."),
     318              :                               role);
     319            0 :         retval = STATUS_ERROR;
     320              :     }
     321              : 
     322            1 :     return retval;
     323              : }
     324              : 
     325              : /*
     326              :  * Check given password for given user, and return STATUS_OK or STATUS_ERROR.
     327              :  *
     328              :  * 'shadow_pass' is the user's correct password hash, as stored in
     329              :  * pg_authid.rolpassword.
     330              :  * 'client_pass' is the password given by the remote user.
     331              :  *
     332              :  * In the error case, store a string at *logdetail that will be sent to the
     333              :  * postmaster log (but not the client).
     334              :  */
     335              : int
     336          108 : plain_crypt_verify(const char *role, const char *shadow_pass,
     337              :                    const char *client_pass,
     338              :                    const char **logdetail)
     339              : {
     340              :     char        crypt_client_pass[MD5_PASSWD_LEN + 1];
     341          108 :     const char *errstr = NULL;
     342              : 
     343              :     /*
     344              :      * Client sent password in plaintext.  If we have an MD5 hash stored, hash
     345              :      * the password the client sent, and compare the hashes.  Otherwise
     346              :      * compare the plaintext passwords directly.
     347              :      */
     348          108 :     switch (get_password_type(shadow_pass))
     349              :     {
     350           27 :         case PASSWORD_TYPE_SCRAM_SHA_256:
     351           27 :             if (scram_verify_plain_password(role,
     352              :                                             client_pass,
     353              :                                             shadow_pass))
     354              :             {
     355           12 :                 return STATUS_OK;
     356              :             }
     357              :             else
     358              :             {
     359           15 :                 *logdetail = psprintf(_("Password does not match for user \"%s\"."),
     360              :                                       role);
     361           15 :                 return STATUS_ERROR;
     362              :             }
     363              :             break;
     364              : 
     365           13 :         case PASSWORD_TYPE_MD5:
     366           13 :             if (!pg_md5_encrypt(client_pass,
     367              :                                 (const uint8 *) role,
     368              :                                 strlen(role),
     369              :                                 crypt_client_pass,
     370              :                                 &errstr))
     371              :             {
     372            0 :                 *logdetail = errstr;
     373            0 :                 return STATUS_ERROR;
     374              :             }
     375           13 :             if (strcmp(crypt_client_pass, shadow_pass) == 0)
     376            5 :                 return STATUS_OK;
     377              :             else
     378              :             {
     379            8 :                 *logdetail = psprintf(_("Password does not match for user \"%s\"."),
     380              :                                       role);
     381            8 :                 return STATUS_ERROR;
     382              :             }
     383              :             break;
     384              : 
     385           68 :         case PASSWORD_TYPE_PLAINTEXT:
     386              : 
     387              :             /*
     388              :              * We never store passwords in plaintext, so this shouldn't
     389              :              * happen.
     390              :              */
     391           68 :             break;
     392              :     }
     393              : 
     394              :     /*
     395              :      * This shouldn't happen.  Plain "password" authentication is possible
     396              :      * with any kind of stored password hash.
     397              :      */
     398           68 :     *logdetail = psprintf(_("Password of user \"%s\" is in unrecognized format."),
     399              :                           role);
     400           68 :     return STATUS_ERROR;
     401              : }
        

Generated by: LCOV version 2.0-1