From 63c71baa7877acf20f97cb80350b9a26e068e8f5 Mon Sep 17 00:00:00 2001 From: Kundan Sable Date: Wed, 22 Jul 2026 16:00:00 +0530 Subject: [PATCH 1/2] fix: don't hard-fail on an undecryptable saved password MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connection._decode_password() let UnicodeDecodeError from decrypt() propagate as a hard error. For OIDC/OAuth2 users whose crypt key can differ between sessions, a password saved under a previous key can never be decoded, and every connection attempt surfaced "Failed to decrypt the saved password" — even after deleting and recreating the pgAdmin user, since the corrupted ciphertext is what's stored, not anything tied to the user record. Catch the decode failure, log a warning, and treat it as "no saved password" instead of propagating the error. connect() then falls through to the normal password prompt, so the account is recoverable instead of permanently stuck. Fixes #10140 --- .../utils/driver/psycopg3/connection.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/web/pgadmin/utils/driver/psycopg3/connection.py b/web/pgadmin/utils/driver/psycopg3/connection.py index ad544e99b25..57b9239dbaa 100644 --- a/web/pgadmin/utils/driver/psycopg3/connection.py +++ b/web/pgadmin/utils/driver/psycopg3/connection.py @@ -258,12 +258,20 @@ def _decode_password(self, encpass, manager, password, crypt_key): if isinstance(password, bytes): password = password.decode() except Exception as e: - manager.stop_ssh_tunnel() - current_app.logger.exception(e) - return True, \ - _( - "Failed to decrypt the saved password.\nError: {0}" - ).format(str(e)), password + # The saved password could not be decrypted. This happens + # when the stored ciphertext was encrypted with a different + # key (e.g. OIDC/OAuth2 logins where the derived encryption + # key changed between sessions), leaving un-decodable bytes + # (typically a "'utf-8' codec can't decode byte 0x.." error). + # Instead of failing every connection attempt permanently, + # discard the bad saved password and continue so the user is + # prompted for the password again. + current_app.logger.warning( + 'Ignoring the saved password as it could not be ' + 'decrypted. The user will be prompted for the password. ' + 'Error: {0}'.format(str(e)) + ) + return False, '', None return False, '', password def connect(self, **kwargs): From 4039f3532bb3f2f0d0630b6e781dd12deb3d2282 Mon Sep 17 00:00:00 2001 From: Kundan Sable Date: Fri, 24 Jul 2026 19:44:41 +0530 Subject: [PATCH 2/2] fix: clear cached ciphertext when a saved password fails to decrypt Per CodeRabbit review on PR #10174: _decode_password() discarded the undecryptable password for the current decode call, but the stale ciphertext was already cached on self.password (set earlier in connect(), before decoding) and never cleared. That meant: - Every subsequent connect() attempt re-decoded and re-warned on the same bad ciphertext instead of the "no saved password" state sticking. - If a server also had passexec_cmd configured, the passexec fallback in connect() (which only runs when neither password nor encpass is set) was skipped, since the stale encpass was still truthy. Clear self.password and manager.password alongside discarding the password in _decode_password's except branch, so the bad ciphertext doesn't linger. The SSH-tunnel prompt path in get_response_for_password() trusting `not server.save_password` (which can be true even for an undecryptable saved password) is a separate, pre-existing issue and is intentionally left out of this narrower fix. --- web/pgadmin/utils/driver/psycopg3/connection.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/pgadmin/utils/driver/psycopg3/connection.py b/web/pgadmin/utils/driver/psycopg3/connection.py index 57b9239dbaa..be1f5678752 100644 --- a/web/pgadmin/utils/driver/psycopg3/connection.py +++ b/web/pgadmin/utils/driver/psycopg3/connection.py @@ -271,6 +271,13 @@ def _decode_password(self, encpass, manager, password, crypt_key): 'decrypted. The user will be prompted for the password. ' 'Error: {0}'.format(str(e)) ) + # Clear the cached ciphertext so it isn't silently reused + # on the next connect() attempt, which would just hit + # this same decode failure again, and (since a stale but + # still-truthy encpass would otherwise remain cached) + # skip the passexec fallback below. + self.password = None + manager.password = None return False, '', None return False, '', password