diff --git a/web/config.py b/web/config.py
index d056ce98b..b4c25c079 100644
--- a/web/config.py
+++ b/web/config.py
@@ -583,7 +583,7 @@ ALLOW_SAVE_TUNNEL_PASSWORD = False
# Applicable for desktop mode only
##########################################################################
MASTER_PASSWORD_REQUIRED = True
-
+USE_OS_SECRET_STORAGE = True
##########################################################################
# pgAdmin encrypts the database connection and ssh tunnel password using a
diff --git a/web/pgadmin/authenticate/kerberos.py b/web/pgadmin/authenticate/kerberos.py
index c0b22e072..3f8db6e67 100644
--- a/web/pgadmin/authenticate/kerberos.py
+++ b/web/pgadmin/authenticate/kerberos.py
@@ -30,7 +30,7 @@ from pgadmin.utils.ajax import make_json_response, internal_server_error
from pgadmin.authenticate.internal import BaseAuthentication
from pgadmin.authenticate import get_auth_sources
from pgadmin.utils.csrf import pgCSRFProtect
-
+from pgadmin.utils.master_password import set_crypt_key
try:
import gssapi
@@ -193,7 +193,8 @@ class KerberosAuthentication(BaseAuthentication):
if status:
# Saving the first 15 characters of the kerberos key
# to encrypt/decrypt database password
- session['pass_enc_key'] = auth_header[1][0:15]
+ pass_enc_key = auth_header[1][0:15]
+ set_crypt_key(pass_enc_key)
# Create user
retval = self.__auto_create_user(
str(negotiate.initiator_name))
diff --git a/web/pgadmin/authenticate/oauth2.py b/web/pgadmin/authenticate/oauth2.py
index b7642bb40..d1ce51a0b 100644
--- a/web/pgadmin/authenticate/oauth2.py
+++ b/web/pgadmin/authenticate/oauth2.py
@@ -26,6 +26,7 @@ from pgadmin.utils import PgAdminModule, get_safe_post_login_redirect, \
get_safe_post_logout_redirect
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.model import db
+from pgadmin.utils.master_password import set_crypt_key
OAUTH2_LOGOUT = 'oauth2.logout'
OAUTH2_AUTHORIZE = 'oauth2.authorize'
@@ -210,7 +211,8 @@ class OAuth2Authentication(BaseAuthentication):
session['oauth2_token'] = self.oauth2_clients[
self.oauth2_current_client].authorize_access_token()
- session['pass_enc_key'] = session['oauth2_token']['access_token']
+ pass_enc_key = session['oauth2_token']['access_token']
+ set_crypt_key(pass_enc_key)
if 'OAUTH2_LOGOUT_URL' in self.oauth2_config[
self.oauth2_current_client]:
diff --git a/web/pgadmin/authenticate/webserver.py b/web/pgadmin/authenticate/webserver.py
index 5a9e4533c..2c9f47e8d 100644
--- a/web/pgadmin/authenticate/webserver.py
+++ b/web/pgadmin/authenticate/webserver.py
@@ -12,7 +12,7 @@
import secrets
import string
import config
-from flask import request, current_app, session, Response, render_template, \
+from flask import request, current_app, Response, render_template, \
url_for
from flask_babel import gettext
from flask_security import login_user
@@ -23,6 +23,7 @@ from pgadmin.utils.constants import WEBSERVER
from pgadmin.utils import PgAdminModule
from pgadmin.utils.csrf import pgCSRFProtect
from flask_security.utils import logout_user
+from pgadmin.utils.master_password import set_crypt_key
class WebserverModule(PgAdminModule):
@@ -89,8 +90,9 @@ class WebserverAuthentication(BaseAuthentication):
return False, gettext(
"Webserver authenticate failed.")
- session['pass_enc_key'] = ''.join(
+ pass_enc_key = ''.join(
(secrets.choice(string.ascii_lowercase) for _ in range(10)))
+ set_crypt_key(pass_enc_key)
useremail = request.environ.get('mail')
if not useremail:
useremail = ''
diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py
index 1867d6e86..1e4200177 100644
--- a/web/pgadmin/browser/__init__.py
+++ b/web/pgadmin/browser/__init__.py
@@ -10,22 +10,21 @@
import json
import logging
import os
+import secrets
import sys
from abc import ABCMeta, abstractmethod
from smtplib import SMTPConnectError, SMTPResponseException, \
SMTPServerDisconnected, SMTPDataError, SMTPHeloError, SMTPException, \
SMTPAuthenticationError, SMTPSenderRefused, SMTPRecipientsRefused
from socket import error as SOCKETErrorException
-from urllib.request import urlopen
-from pgadmin.utils.constants import KEY_RING_SERVICE_NAME, \
- KEY_RING_USERNAME_FORMAT, KEY_RING_DESKTOP_USER, KEY_RING_TUNNEL_FORMAT, \
- MessageType
-
-import time
import keyring
+from keyring.errors import KeyringLocked
+from pgadmin.utils.constants import KEY_RING_SERVICE_NAME, \
+ KEY_RING_USER_NAME,MessageType
+
from flask import current_app, render_template, url_for, make_response, \
- flash, Response, request, after_this_request, redirect, session
+ flash, Response, request, redirect, session
from flask_babel import gettext
from libgravatar import Gravatar
from flask_security import current_user
@@ -44,22 +43,24 @@ from werkzeug.datastructures import MultiDict
import config
from pgadmin import current_blueprint
from pgadmin.authenticate import get_logout_url
-from pgadmin.authenticate.mfa.utils import mfa_required, is_mfa_enabled
-from pgadmin.settings import get_setting, store_setting
+from pgadmin.authenticate.mfa.utils import is_mfa_enabled
+from pgadmin.settings import get_setting
from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_json_response, internal_server_error, \
bad_request
from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.utils.preferences import Preferences
-from pgadmin.utils.menu import MenuItem
from pgadmin.browser.register_browser_preferences import \
register_browser_preferences
from pgadmin.utils.master_password import validate_master_password, \
set_masterpass_check_text, cleanup_master_password, get_crypt_key, \
- set_crypt_key, process_masterpass_disabled
+ set_crypt_key, process_masterpass_disabled, \
+ delete_local_storage_master_key, \
+ get_master_password_key_from_os_secret, \
+ get_master_password_from_master_hook
from pgadmin.model import User, db
-from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE,\
- INTERNAL, KERBEROS, LDAP, QT_DEFAULT_PLACEHOLDER, OAUTH2, WEBSERVER,\
+from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE, \
+ INTERNAL, KERBEROS, LDAP, QT_DEFAULT_PLACEHOLDER, OAUTH2, WEBSERVER, \
VW_EDT_DEFAULT_PLACEHOLDER
from pgadmin.authenticate import AuthSourceManager
from pgadmin.utils.exception import CryptKeyMissing
@@ -74,7 +75,7 @@ PGADMIN_BROWSER = 'pgAdmin.Browser'
PASS_ERROR_MSG = gettext('Your password has not been changed.')
SMTP_SOCKET_ERROR = gettext(
'SMTP Socket error: {error}\n {pass_error}').format(
- error={}, pass_error=PASS_ERROR_MSG)
+ error={}, pass_error=PASS_ERROR_MSG)
SMTP_ERROR = gettext('SMTP error: {error}\n {pass_error}').format(
error={}, pass_error=PASS_ERROR_MSG)
PASS_ERROR = gettext('Error: {error}\n {pass_error}').format(
@@ -631,14 +632,13 @@ def get_nodes():
def form_master_password_response(existing=True, present=False, errmsg=None,
- keyring_name='',
- invalid_master_password_hook=False):
+ keyring_name='', master_password_hook=''):
return make_json_response(data={
'present': present,
'reset': existing,
'errmsg': errmsg,
'keyring_name': keyring_name,
- 'invalid_master_password_hook': invalid_master_password_hook,
+ 'master_password_hook': master_password_hook,
'is_error': True if errmsg else False
})
@@ -673,14 +673,11 @@ def reset_master_password():
Removes the master password and remove all saved passwords
This password will be used to encrypt/decrypt saved server passwords
"""
- if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
- # This is to set the Desktop user password so it will not ask for
- # migrate exiting passwords as those are getting cleared
- keyring.set_password(KEY_RING_SERVICE_NAME,
- KEY_RING_DESKTOP_USER.format(
- current_user.username), 'test')
cleanup_master_password()
status, crypt_key = get_crypt_key()
+ if not status and config.MASTER_PASSWORD_HOOK:
+ crypt_key = get_master_password_from_master_hook()
+
# Set masterpass_check if MASTER_PASSWORD_HOOK is set which provides
# encryption key
if config.MASTER_PASSWORD_REQUIRED and config.MASTER_PASSWORD_HOOK:
@@ -695,9 +692,7 @@ def set_master_password():
Set the master password and store in the memory
This password will be used to encrypt/decrypt saved server passwords
"""
-
data = None
-
if request.form:
data = request.form
elif request.data:
@@ -708,130 +703,105 @@ def set_master_password():
if data != '':
data = json.loads(data)
- if not config.DISABLED_LOCAL_PASSWORD_STORAGE and \
- (config.ALLOW_SAVE_PASSWORD or config.ALLOW_SAVE_TUNNEL_PASSWORD):
- if data.get('password') and config.MASTER_PASSWORD_REQUIRED and\
- not validate_master_password(data.get('password')):
- return form_master_password_response(
- present=False,
- keyring_name=config.KEYRING_NAME,
- errmsg=gettext("Incorrect master password")
- )
- from pgadmin.model import Server
- from pgadmin.utils.crypto import decrypt
- desktop_user = current_user
+ keyring_name = ''
+ errmsg = ''
+ if not config.SERVER_MODE:
+ if config.USE_OS_SECRET_STORAGE:
+ try:
+ # Try to get master key is from local os storage
+ master_key = get_master_password_key_from_os_secret()
+ master_password = data.get('password', None)
+ keyring_name = config.KEYRING_NAME
+ if not master_key:
+ # Generate new one and migration required
+ master_key = secrets.token_urlsafe(12)
- enc_key = data['password']
- if not config.MASTER_PASSWORD_REQUIRED:
- status, enc_key = get_crypt_key()
- if not status:
- raise CryptKeyMissing
+ # migrate existing server passwords
+ from pgadmin.browser.server_groups.servers.utils \
+ import migrate_saved_passwords
+ migrated_save_passwords, error = migrate_saved_passwords(
+ master_key, master_password)
- try:
- all_server = Server.query.all()
- saved_password_servers = [server for server in all_server if
- server.save_password]
- # pgAdmin will use the OS password manager to store the server
- # password, here migrating the existing saved server password to
- # OS password manager
- if len(saved_password_servers) > 0 and (keyring.get_password(
- KEY_RING_SERVICE_NAME, KEY_RING_DESKTOP_USER.format(
- desktop_user.username)) or enc_key):
- is_migrated = False
-
- for server in saved_password_servers:
- if enc_key:
- if server.password and config.ALLOW_SAVE_PASSWORD:
- name = KEY_RING_USERNAME_FORMAT.format(server.name,
- server.id)
- password = decrypt(server.password,
- enc_key).decode()
- # Store the password using OS password manager
- keyring.set_password(KEY_RING_SERVICE_NAME, name,
- password)
- is_migrated = True
- setattr(server, 'password', None)
-
- if server.tunnel_password and \
- config.ALLOW_SAVE_TUNNEL_PASSWORD:
- tname = KEY_RING_TUNNEL_FORMAT.format(server.name,
- server.id)
- tpassword = decrypt(server.tunnel_password,
- enc_key).decode()
- # Store the password using OS password manager
- keyring.set_password(KEY_RING_SERVICE_NAME, tname,
- tpassword)
- is_migrated = True
- setattr(server, 'tunnel_password', None)
-
- db.session.commit()
-
- # Store the password using OS password manager
- keyring.set_password(KEY_RING_SERVICE_NAME,
- KEY_RING_DESKTOP_USER.format(
- desktop_user.username), 'test')
- return form_master_password_response(
- existing=True,
- present=True,
- keyring_name=config.KEYRING_NAME if is_migrated else ''
- )
- else:
- if len(all_server) == 0:
- # Store the password using OS password manager
- keyring.set_password(KEY_RING_SERVICE_NAME,
- KEY_RING_DESKTOP_USER.format(
- desktop_user.username), 'test')
- return form_master_password_response(
- present=True,
- )
- else:
- is_master_password_present = True
- keyring_name = ''
- for server in all_server:
- is_password_present = \
- server.save_password or server.tunnel_password
- if server.password and is_password_present:
- is_master_password_present = False
- keyring_name = config.KEYRING_NAME
- break
-
- if is_master_password_present:
- # Store the password using OS password manager
+ if migrated_save_passwords:
+ # Update keyring
keyring.set_password(KEY_RING_SERVICE_NAME,
- KEY_RING_DESKTOP_USER.format(
- desktop_user.username),
- 'test')
-
+ KEY_RING_USER_NAME,
+ master_key)
+ # set crypt key
+ set_crypt_key(master_key)
+ return form_master_password_response(
+ existing=True,
+ present=True,
+ keyring_name=keyring_name)
+ else:
+ if not error:
+ set_crypt_key(master_key)
+ return form_master_password_response(
+ present=True)
+ # Migration failed
+ elif error != 'Master password required':
+ errmsg = error
+ return form_master_password_response(
+ existing=False,
+ present=True,
+ errmsg=errmsg,
+ keyring_name=keyring_name)
+ else:
+ current_app.logger.warning(
+ ' Master key was already present in the keyring,'
+ ' hence not doing any migration')
+ # Key is already generated and set, no migration required
+ # set crypt key
+ set_crypt_key(master_key)
return form_master_password_response(
- present=is_master_password_present,
- keyring_name=keyring_name
- )
- except Exception as e:
- current_app.logger.warning(
- 'Fail set password using OS password manager'
- ', fallback to master password. Error: {0}'.format(e)
- )
- config.DISABLED_LOCAL_PASSWORD_STORAGE = True
-
- # If the master password is required and the master password hook
- # is specified then try to retrieve the encryption key and update data.
- # If there is an error while retrieving it, return an error message.
- if config.SERVER_MODE and config.MASTER_PASSWORD_REQUIRED and \
- config.MASTER_PASSWORD_HOOK:
- status, enc_key = get_crypt_key()
- if status:
- data = {'password': enc_key, 'submit_password': True}
+ present=True)
+ except KeyringLocked as e:
+ current_app.logger.warning(
+ 'Failed to set because Access Denied.'
+ ' Error: {0}'.format(e))
+ config.USE_OS_SECRET_STORAGE = False
+ except Exception as e:
+ current_app.logger.warning(
+ 'Failed to set encryption key using OS password manager'
+ ', fallback to master password. Error: {0}'.format(e))
+ # Also if masterpass_check is none it means previously
+ # passwords were migrated using keyring crypt key.
+ # Reset all passwords because we are going to master password
+ # again and while setting master password, all server
+ # passwords are decrypted using old key before re-encryption
+ if current_user.masterpass_check is None:
+ from pgadmin.browser.server_groups.servers.utils \
+ import remove_saved_passwords, update_session_manager
+ remove_saved_passwords(current_user.id)
+ update_session_manager(current_user.id)
+ # Disable local os storage if any exception while creation
+ config.USE_OS_SECRET_STORAGE = False
+ delete_local_storage_master_key()
else:
- error = gettext('The master password could not be retrieved from '
- 'the MASTER_PASSWORD_HOOK utility specified {0}.'
- 'Please check that the hook utility is configured'
- ' correctly.'.format(config.MASTER_PASSWORD_HOOK))
- return form_master_password_response(
- existing=False,
- present=False,
- errmsg=error,
- invalid_master_password_hook=True
- )
+ # if os secret storage disabled now, but was used once then
+ # remove all the saved passwords
+ delete_local_storage_master_key()
+ else:
+ # If the master password is required and the master password hook
+ # is specified then try to retrieve the encryption key and update data.
+ # If there is an error while retrieving it, return an error message.
+ if config.MASTER_PASSWORD_REQUIRED and config.MASTER_PASSWORD_HOOK:
+ master_password = get_master_password_from_master_hook()
+ if master_password:
+ data = {'password': master_password, 'submit_password': True}
+ else:
+ errmsg = gettext(
+ 'The master password could not be retrieved from the'
+ ' MASTER_PASSWORD_HOOK utility specified {0}. Please check'
+ ' that the hook utility is configured correctly.'.format(
+ config.MASTER_PASSWORD_HOOK))
+ return form_master_password_response(
+ existing=False,
+ present=False,
+ errmsg=errmsg,
+ master_password_hook=config.MASTER_PASSWORD_HOOK,
+ keyring_name=keyring_name
+ )
# Master password is applicable for Desktop mode and in server mode
# only when auth sources are oauth, kerberos, webserver.
@@ -843,20 +813,16 @@ def set_master_password():
if current_user.masterpass_check is not None and \
data.get('submit_password', False) and \
not validate_master_password(data.get('password')):
- errmsg = '' if config.MASTER_PASSWORD_HOOK \
- else gettext("Incorrect master password")
- invalid_master_password_hook = \
- True if config.MASTER_PASSWORD_HOOK else False
return form_master_password_response(
existing=True,
present=False,
errmsg=errmsg,
- invalid_master_password_hook=invalid_master_password_hook
+ master_password_hook=config.MASTER_PASSWORD_HOOK,
+ keyring_name=keyring_name
)
# if master password received in request
if data != '' and data.get('password', '') != '':
-
# store the master pass in the memory
set_crypt_key(data.get('password'))
@@ -864,7 +830,6 @@ def set_master_password():
# master check is not set, which means the server password
# data is old and is encrypted with old key
# Re-encrypt with new key
-
from pgadmin.browser.server_groups.servers.utils \
import reencrpyt_server_passwords
reencrpyt_server_passwords(
@@ -877,13 +842,14 @@ def set_master_password():
# If password in request is empty then try to get it with
# get_crypt_key method. If get_crypt_key() returns false status and
- # masterpass_check is already set, provide a pop to enter
+ # masterpass_check is already set, provide a popup to enter
# master password(present) without the reset option.(existing).
elif not get_crypt_key()[0] and \
current_user.masterpass_check is not None:
return form_master_password_response(
existing=True,
present=False,
+ keyring_name=keyring_name
)
# If get_crypt_key return True,but crypt_key is none and
@@ -896,7 +862,8 @@ def set_master_password():
return form_master_password_response(
existing=False,
present=False,
- errmsg=error_message
+ errmsg=error_message,
+ keyring_name=keyring_name
)
# if master password is disabled now, but was used once then
@@ -904,7 +871,6 @@ def set_master_password():
process_masterpass_disabled()
if config.SERVER_MODE and current_user.masterpass_check is None:
-
crypt_key = get_crypt_key()[1]
from pgadmin.browser.server_groups.servers.utils \
import reencrpyt_server_passwords
@@ -921,8 +887,6 @@ def set_master_password():
# Only register route if SECURITY_CHANGEABLE is set to True
# We can't access app context here so cannot
# use app.config['SECURITY_CHANGEABLE']
-
-
if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE:
@blueprint.route("/change_password", endpoint="change_password",
methods=['GET', 'POST'])
diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py
index 8209a46c5..c1c956522 100644
--- a/web/pgadmin/browser/server_groups/servers/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/__init__.py
@@ -38,11 +38,8 @@ from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
SERVER_CONNECTION_CLOSED
from sqlalchemy import or_
from pgadmin.utils.preferences import Preferences
-from pgadmin.utils.constants import KEY_RING_SERVICE_NAME, \
- KEY_RING_USERNAME_FORMAT, KEY_RING_TUNNEL_FORMAT, KEY_RING_DESKTOP_USER
from .... import socketio as sio
from pgadmin.utils import get_complete_file_path
-import keyring
def has_any(data, keys):
@@ -255,22 +252,6 @@ class ServerModule(sg.ServerGroupPluginModule):
except Exception as e:
current_app.logger.exception(e)
errmsg = str(e)
-
- is_password_saved = bool(server.save_password)
- is_tunnel_password_saved = bool(server.tunnel_password)
-
- if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
- sname = KEY_RING_USERNAME_FORMAT.format(server.name, server.id)
- spassword = keyring.get_password(
- KEY_RING_SERVICE_NAME, sname)
-
- is_password_saved = bool(spassword)
- tunnelname = KEY_RING_TUNNEL_FORMAT.format(server.name,
- server.id)
- tunnel_password = keyring.get_password(KEY_RING_SERVICE_NAME,
- tunnelname)
- is_tunnel_password_saved = bool(tunnel_password)
-
yield self.generate_browser_node(
"%d" % (server.id),
gid,
@@ -287,8 +268,8 @@ class ServerModule(sg.ServerGroupPluginModule):
wal_pause=wal_paused,
host=server.host,
port=server.port,
- is_password_saved=is_password_saved,
- is_tunnel_password_saved=is_tunnel_password_saved,
+ is_password_saved=bool(server.save_password),
+ is_tunnel_password_saved=bool(server.tunnel_password),
was_connected=was_connected,
errmsg=errmsg,
user_id=server.user_id,
@@ -605,22 +586,6 @@ class ServerNode(PGChildNodeView):
manager.release()
errmsg = "{0} : {1}".format(server.name, result)
- is_password_saved = bool(server.save_password)
- is_tunnel_password_saved = bool(server.tunnel_password)
-
- if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
- sname = KEY_RING_USERNAME_FORMAT.format(server.name, server.id)
- spassword = keyring.get_password(
- KEY_RING_SERVICE_NAME, sname)
-
- is_password_saved = bool(spassword)
-
- tunnelname = KEY_RING_TUNNEL_FORMAT.format(server.name,
- server.id)
- tunnel_password = keyring.get_password(KEY_RING_SERVICE_NAME,
- tunnelname)
- is_tunnel_password_saved = bool(tunnel_password)
-
res.append(
self.blueprint.generate_browser_node(
"%d" % (server.id),
@@ -637,8 +602,8 @@ class ServerNode(PGChildNodeView):
user=manager.user_info if connected else None,
in_recovery=in_recovery,
wal_pause=wal_paused,
- is_password_saved=is_password_saved,
- is_tunnel_password_saved=is_tunnel_password_saved,
+ is_password_saved=bool(server.save_password),
+ is_tunnel_password_saved=bool(server.tunnel_password),
errmsg=errmsg,
username=server.username,
shared=server.shared,
@@ -761,20 +726,6 @@ class ServerNode(PGChildNodeView):
server_name = s.name
get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id)
db.session.delete(s)
- if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
- try:
- sname = KEY_RING_USERNAME_FORMAT.format(
- s.name,
- s.id)
- # Get password form OS password manager
- is_present = keyring.get_password(
- KEY_RING_SERVICE_NAME, sname)
- # Delete saved password from OS password manager
- if is_present:
- keyring.delete_password(KEY_RING_SERVICE_NAME,
- sname)
- except keyring.errors.KeyringError as e:
- config.DISABLED_LOCAL_PASSWORD_STORAGE = True
db.session.commit()
self.delete_shared_server(server_name, gid, sid)
QueryHistory.clear_history(current_user.id, sid)
@@ -888,26 +839,6 @@ class ServerNode(PGChildNodeView):
)
try:
- if len(old_server_name) and old_server_name != server.name and \
- not config.DISABLED_LOCAL_PASSWORD_STORAGE and \
- server.save_password:
- # If server name is changed then update keyring with
- # new server name
- password = keyring.get_password(
- KEY_RING_SERVICE_NAME,
- KEY_RING_USERNAME_FORMAT.format(old_server_name,
- server.id))
-
- keyring.set_password(
- KEY_RING_SERVICE_NAME,
- KEY_RING_USERNAME_FORMAT.format(server.name, server.id),
- password)
-
- server_name = KEY_RING_USERNAME_FORMAT.format(
- old_server_name, server.id)
- # Delete saved password from OS password manager
- keyring.delete_password(KEY_RING_SERVICE_NAME,
- server_name)
db.session.commit()
except Exception as e:
current_app.logger.exception(e)
@@ -1175,11 +1106,10 @@ class ServerNode(PGChildNodeView):
if data[item] == '':
data[item] = None
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- # Get enc key
- crypt_key_present, crypt_key = get_crypt_key()
- if not crypt_key_present:
- raise CryptKeyMissing
+ # Get enc key
+ crypt_key_present, crypt_key = get_crypt_key()
+ if not crypt_key_present:
+ raise CryptKeyMissing
# Some fields can be provided with service file so they are optional
if 'service' in data and not data['service']:
@@ -1276,9 +1206,7 @@ class ServerNode(PGChildNodeView):
# login with password
have_password = True
password = data['password']
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- password = encrypt(password, crypt_key)
-
+ password = encrypt(password, crypt_key)
elif 'passfile' in data['connection_params'] and \
data['connection_params']['passfile'] != '':
passfile = data['connection_params']['passfile']
@@ -1286,10 +1214,7 @@ class ServerNode(PGChildNodeView):
if 'tunnel_password' in data and data["tunnel_password"] != '':
have_tunnel_password = True
tunnel_password = data['tunnel_password']
-
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- tunnel_password = \
- encrypt(tunnel_password, crypt_key)
+ tunnel_password = encrypt(tunnel_password, crypt_key)
status, errmsg = conn.connect(
password=password,
@@ -1310,32 +1235,15 @@ class ServerNode(PGChildNodeView):
else:
if 'save_password' in data and data['save_password'] and \
have_password and config.ALLOW_SAVE_PASSWORD:
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- setattr(server, 'password', password)
- db.session.commit()
- else:
- # Store the password using OS password manager
- keyring.set_password(
- KEY_RING_SERVICE_NAME,
- KEY_RING_USERNAME_FORMAT.format(server.name,
- server.id),
- password)
+ setattr(server, 'password', password)
+ db.session.commit()
if 'save_tunnel_password' in data and \
data['save_tunnel_password'] and \
have_tunnel_password and \
config.ALLOW_SAVE_TUNNEL_PASSWORD:
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- setattr(server, 'tunnel_password', tunnel_password)
- db.session.commit()
- else:
- # Store the password using OS password manager
- keyring.set_password(
- KEY_RING_SERVICE_NAME,
- KEY_RING_TUNNEL_FORMAT.format(server.name,
- server.id),
- tunnel_password)
- tunnel_password_saved = True
+ setattr(server, 'tunnel_password', tunnel_password)
+ db.session.commit()
replication_type = get_replication_type(conn,
manager.version)
@@ -1540,39 +1448,18 @@ class ServerNode(PGChildNodeView):
conn = manager.connection()
crypt_key = None
- if server.save_password:
- if config.DISABLED_LOCAL_PASSWORD_STORAGE or \
- not keyring.get_password(
- KEY_RING_SERVICE_NAME,
- KEY_RING_DESKTOP_USER.format(current_user.username)):
- crypt_key_present, crypt_key = get_crypt_key()
- if not crypt_key_present:
- raise CryptKeyMissing
-
- else:
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- # Get enc key
- crypt_key_present, crypt_key = get_crypt_key()
- if not crypt_key_present:
- raise CryptKeyMissing
+ # Get enc key
+ crypt_key_present, crypt_key = get_crypt_key()
+ if not crypt_key_present:
+ raise CryptKeyMissing
# If server using SSH Tunnel
if server.use_ssh_tunnel:
-
if 'tunnel_password' not in data:
- if config.DISABLED_LOCAL_PASSWORD_STORAGE \
- and server.tunnel_password is None:
+ if server.tunnel_password is None:
prompt_tunnel_password = True
else:
- if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
- # Get password form OS password manager
- tunnel_password = keyring.get_password(
- KEY_RING_SERVICE_NAME,
- KEY_RING_TUNNEL_FORMAT.format(server.name,
- server.id))
- prompt_tunnel_password = not bool(tunnel_password)
- else:
- tunnel_password = server.tunnel_password
+ tunnel_password = server.tunnel_password
else:
tunnel_password = data['tunnel_password'] \
if 'tunnel_password' in data else ''
@@ -1582,11 +1469,9 @@ class ServerNode(PGChildNodeView):
# Encrypt the password before saving with user's login
# password key.
try:
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- tunnel_password = encrypt(tunnel_password, crypt_key) \
- if tunnel_password is not None else \
- server.tunnel_password
-
+ tunnel_password = encrypt(tunnel_password, crypt_key) \
+ if tunnel_password is not None else \
+ server.tunnel_password
except Exception as e:
current_app.logger.exception(e)
return internal_server_error(errormsg=str(e))
@@ -1608,43 +1493,20 @@ class ServerNode(PGChildNodeView):
get_complete_file_path(passfile_param):
passfile = passfile_param
else:
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- password = conn_passwd or server.password
- else:
- # Get password form OS password manager
- password = keyring.get_password(
- KEY_RING_SERVICE_NAME,
- KEY_RING_USERNAME_FORMAT.format(server.name,
- server.id))
- prompt_password = (
- True
- if password is None and server.passexec_cmd is None
- else False
- )
+ password = conn_passwd or server.password
else:
password = data['password'] if 'password' in data else None
save_password = data['save_password']\
if 'save_password' in data else False
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- try:
- # Encrypt the password before saving with user's login
- # password key.
- password = encrypt(password, crypt_key) \
- if password is not None else server.password
- except Exception as e:
- current_app.logger.exception(e)
- return internal_server_error(errormsg=str(e))
- elif save_password and config.ALLOW_SAVE_PASSWORD:
- # Store the password using OS password manager
- keyring.set_password(
- KEY_RING_SERVICE_NAME,
- KEY_RING_USERNAME_FORMAT.format(
- server.name, server.id), password)
- # Get password form OS password manager
- password = keyring.get_password(
- KEY_RING_SERVICE_NAME,
- KEY_RING_USERNAME_FORMAT.format(server.name, server.id))
+ try:
+ # Encrypt the password before saving with user's login
+ # password key.
+ password = encrypt(password, crypt_key) \
+ if password is not None else server.password
+ except Exception as e:
+ current_app.logger.exception(e)
+ return internal_server_error(errormsg=str(e))
# Check do we need to prompt for the database server or ssh tunnel
# password or both. Return the password template in case password is
@@ -1691,7 +1553,7 @@ class ServerNode(PGChildNodeView):
# Save the encrypted password using the user's login
# password key, if there is any password to save
- if password and config.DISABLED_LOCAL_PASSWORD_STORAGE:
+ if password:
if server.shared and server.user_id != current_user.id:
setattr(shared_server, 'password', password)
else:
@@ -1707,18 +1569,8 @@ class ServerNode(PGChildNodeView):
if save_tunnel_password and config.ALLOW_SAVE_TUNNEL_PASSWORD:
try:
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- # Save the encrypted tunnel password.
- setattr(server, 'tunnel_password', tunnel_password)
- else:
- # Store the password using OS password manager
- keyring.set_password(
- KEY_RING_SERVICE_NAME,
- KEY_RING_TUNNEL_FORMAT.format(server.name,
- server.id),
- tunnel_password)
- setattr(server, 'tunnel_password', None)
-
+ # Save the encrypted tunnel password.
+ setattr(server, 'tunnel_password', tunnel_password)
db.session.commit()
except Exception as e:
# Release Connection
@@ -1881,28 +1733,16 @@ class ServerNode(PGChildNodeView):
elif request.data:
data = json.loads(request.data)
- crypt_key = None
-
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- # Get enc key
- crypt_key_present, crypt_key = get_crypt_key()
- if not crypt_key_present:
- raise CryptKeyMissing
+ # Get enc key
+ crypt_key_present, crypt_key = get_crypt_key()
+ if not crypt_key_present:
+ raise CryptKeyMissing
# Fetch Server Details
server = Server.query.filter_by(id=sid).first()
-
if server is None:
return bad_request(self.not_found_error_msg())
- spassword = None
- if not config.DISABLED_LOCAL_PASSWORD_STORAGE and \
- bool(server.save_password):
- sname = KEY_RING_USERNAME_FORMAT.format(server.name,
- server.id)
- spassword = keyring.get_password(
- KEY_RING_SERVICE_NAME, sname)
-
# Fetch User Details.
user = User.query.filter_by(id=current_user.id).first()
if user is None:
@@ -1914,9 +1754,9 @@ class ServerNode(PGChildNodeView):
# If there is no password found for the server
# then check for pgpass file
- if (not server.password or spassword) and \
- not manager.password and hasattr(server, 'connection_params') \
- and 'passfile' in server.connection_params and \
+ if not server.password and not manager.password and \
+ hasattr(server, 'connection_params') and \
+ 'passfile' in server.connection_params and \
manager.get_connection_param_value('passfile') and \
server.connection_params['passfile'] == \
manager.get_connection_param_value('passfile'):
@@ -1956,17 +1796,9 @@ class ServerNode(PGChildNodeView):
# Check against old password only if no pgpass file
if not is_passfile:
- if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
- if spassword:
- decrypted_password = spassword
- else:
- decrypted_password = manager.password
- else:
- decrypted_password = decrypt(manager.password, crypt_key)
-
- if isinstance(decrypted_password, bytes):
- decrypted_password = decrypted_password.decode()
-
+ decrypted_password = decrypt(manager.password, crypt_key)
+ if isinstance(decrypted_password, bytes):
+ decrypted_password = decrypted_password.decode()
password = data['password']
# Validate old password before setting new.
@@ -1998,17 +1830,7 @@ class ServerNode(PGChildNodeView):
# Store password in sqlite only if no pgpass file
if not is_passfile:
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- password = encrypt(data['newPassword'], crypt_key)
- elif not config.DISABLED_LOCAL_PASSWORD_STORAGE:
- if config.ALLOW_SAVE_PASSWORD and bool(
- server.save_password):
- keyring.set_password(
- KEY_RING_SERVICE_NAME,
- KEY_RING_USERNAME_FORMAT.format(server.name,
- server.id),
- data['newPassword'])
- password = data['newPassword']
+ password = encrypt(data['newPassword'], crypt_key)
# Check if old password was stored in pgadmin4 sqlite database.
# If yes then update that password.
if server.password is not None and config.ALLOW_SAVE_PASSWORD:
@@ -2232,25 +2054,10 @@ class ServerNode(PGChildNodeView):
server = ServerModule. \
get_shared_server_properties(server, shared_server)
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- if server.shared and server.user_id != current_user.id:
- setattr(shared_server, 'password', None)
- else:
- setattr(server, 'password', None)
+ if server.shared and server.user_id != current_user.id:
+ setattr(shared_server, 'password', None)
else:
- try:
- server_name = KEY_RING_USERNAME_FORMAT.format(server.name,
- server.id)
- # Get password form OS password manager
- is_present = keyring.get_password(KEY_RING_SERVICE_NAME,
- server_name)
- if is_present:
- # Delete saved password from OS password manager
- keyring.delete_password(KEY_RING_SERVICE_NAME,
- server_name)
- except keyring.errors.KeyringError as e:
- config.DISABLED_LOCAL_PASSWORD_STORAGE = True
- setattr(server, 'save_password', None)
+ setattr(server, 'password', None)
# If password was saved then clear the flag also
# 0 is False in SQLite db
@@ -2289,19 +2096,8 @@ class ServerNode(PGChildNodeView):
success=0,
info=self.not_found_error_msg()
)
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- setattr(server, 'tunnel_password', None)
- db.session.commit()
- else:
- server_name = KEY_RING_TUNNEL_FORMAT.format(server.name,
- server.id)
- # Get password form OS password manager
- is_present = keyring.get_password(KEY_RING_SERVICE_NAME,
- server_name)
- if is_present:
- # Delete saved password from OS password manager
- keyring.delete_password(KEY_RING_SERVICE_NAME, server_name)
- setattr(server, 'tunnel_password', None)
+ setattr(server, 'tunnel_password', None)
+ db.session.commit()
except Exception as e:
current_app.logger.error(
"Unable to clear ssh tunnel password."
diff --git a/web/pgadmin/browser/server_groups/servers/utils.py b/web/pgadmin/browser/server_groups/servers/utils.py
index d5c869b55..e34bfa2db 100644
--- a/web/pgadmin/browser/server_groups/servers/utils.py
+++ b/web/pgadmin/browser/server_groups/servers/utils.py
@@ -9,12 +9,20 @@
"""Server helper utilities"""
from ipaddress import ip_address
+import keyring
+from flask_login import current_user
from werkzeug.exceptions import InternalServerError
from flask import render_template
-
+from pgadmin.utils.constants import KEY_RING_USERNAME_FORMAT, \
+ KEY_RING_SERVICE_NAME, KEY_RING_USER_NAME, KEY_RING_TUNNEL_FORMAT, \
+ KEY_RING_DESKTOP_USER
from pgadmin.utils.crypto import encrypt, decrypt
import config
from pgadmin.model import db, Server
+from flask import current_app
+from pgadmin.utils.exception import CryptKeyMissing
+from pgadmin.utils.master_password import validate_master_password, \
+ get_crypt_key, set_masterpass_check_text
def is_valid_ipaddress(address):
@@ -232,6 +240,157 @@ def _password_check(server, manager, old_key, new_key):
manager.password = password
+def migrate_passwords_from_os_secret_storage(servers, enc_key):
+ """
+ Migrate password stored in os secret storage
+ :param servers: server list
+ :param enc_key: new encryption key
+ :return: True if successful else False
+ """
+ passwords_migrated = False
+ error = ''
+ try:
+ if len(servers) > 0:
+ for server in servers:
+ server_name = KEY_RING_USERNAME_FORMAT.format(server.name,
+ server.id)
+ server_password = keyring.get_password(
+ KEY_RING_SERVICE_NAME, server_name)
+ if server_password:
+ server_password = encrypt(server_password, enc_key)
+ setattr(server, 'password', server_password)
+ else:
+ setattr(server, 'save_password', 0)
+
+ tunnel_name = KEY_RING_TUNNEL_FORMAT.format(server.name,
+ server.id)
+ tunnel_password = keyring.get_password(
+ KEY_RING_SERVICE_NAME, tunnel_name)
+ if tunnel_password:
+ setattr(server, 'tunnel_password', tunnel_password)
+ keyring.delete_password(
+ KEY_RING_SERVICE_NAME, tunnel_name)
+ else:
+ setattr(server, 'tunnel_password', None)
+ passwords_migrated = True
+ except Exception as e:
+ error = 'Failed to migrate passwords stored using OS' \
+ ' password manager.Error: {0}'.format(e)
+ current_app.logger.warning(error)
+ return passwords_migrated, error
+
+
+def migrate_passwords_from_pgadmin_db(servers, old_key, enc_key):
+ """
+ Migrates passwords stored in pgadmin db
+ :param servers: list of servers
+ :param old_key: old encryption key
+ :param enc_key: new encryption key
+ :return: True if successful else False
+ """
+ error = ''
+ passwords_migrated = False
+ try:
+ for ser in servers:
+ if ser.password:
+ password = decrypt(ser.password, old_key).decode()
+ server_password = encrypt(password, enc_key)
+ setattr(ser, 'password', server_password)
+
+ if ser.tunnel_password:
+ password = decrypt(ser.tunnel_password, old_key).decode()
+ tunnel_password = encrypt(password, enc_key)
+ setattr(ser, 'tunnel_password', tunnel_password)
+ passwords_migrated = True
+ except Exception as e:
+ error = 'Failed to migrate passwords stored using master password or' \
+ ' user password password manager. Error: {0}'.format(e)
+ current_app.logger.warning(error)
+ config.USE_OS_SECRET_STORAGE = False
+
+ return passwords_migrated, error
+
+
+def migrate_saved_passwords(master_key, master_password):
+ """
+ Function will migrate password stored in pgadmin db and os secret storage
+ with separate entry for each server(initial keyring implementation #5123).
+ Now all saved passwords will be stored in pgadmin db which are encrypted
+ using master_key which is stored in local os storage.
+ :param master_key: encryption key from local os storage
+ :param master_password: set by user if MASTER_PASSWORD_REQUIRED=True
+ :param old_crypt_key: enc_key with ith passwords were encrypted when
+ MASTER_PASSWORD_REQUIRED=False
+ :return: True if all passwords are migrated successfully.
+ """
+ error = ''
+ old_key = None
+ passwords_migrated = False
+ if config.ALLOW_SAVE_PASSWORD or config.ALLOW_SAVE_TUNNEL_PASSWORD:
+ # Get servers with saved password
+ all_server = Server.query.all()
+ saved_password_servers = [ser for ser in all_server
+ if ser.save_password or ser.tunnel_password]
+
+ servers_with_pwd_in_os_secret = []
+ servers_with_pwd_in_pgadmin_db = []
+ for ser in saved_password_servers:
+ if ser.password is None:
+ servers_with_pwd_in_os_secret.append(ser)
+ else:
+ servers_with_pwd_in_pgadmin_db.append(ser)
+
+ # No server passwords are saved
+ if len(saved_password_servers) == 0:
+ current_app.logger.warning(
+ 'There are no saved passwords')
+ return passwords_migrated, error
+
+ # If not master password received return and follow
+ # normal Master password path
+ if config.MASTER_PASSWORD_REQUIRED:
+ if current_user.masterpass_check is not None and \
+ not master_password:
+ error = 'Master password required'
+ return passwords_migrated, error
+ elif master_password:
+ old_key = master_password
+ else:
+ old_key = current_user.password
+
+ # servers passwords stored with os storage are present.
+ if len(servers_with_pwd_in_os_secret) > 0:
+ current_app.logger.warning(
+ 'Re-encrypting passwords saved using os password manager')
+ passwords_migrated, error = \
+ migrate_passwords_from_os_secret_storage(
+ servers_with_pwd_in_os_secret, master_key)
+
+ if len(servers_with_pwd_in_pgadmin_db) > 0 and old_key:
+ # if master_password present and masterpass_check is present,
+ # server passwords are encrypted with master password
+ current_app.logger.warning(
+ 'Re-encrypting passwords saved using master password')
+ passwords_migrated, error = migrate_passwords_from_pgadmin_db(
+ servers_with_pwd_in_pgadmin_db, old_key, master_key)
+ # clear master_pass check once passwords are migrated
+ if passwords_migrated:
+ set_masterpass_check_text('', clear=True)
+
+ if passwords_migrated:
+ # commit the changes once all are migrated
+ db.session.commit()
+ # Delete passwords from os password manager
+ if len(servers_with_pwd_in_os_secret) > 0:
+ delete_saved_passwords_from_os_secret_storage(
+ servers_with_pwd_in_os_secret)
+ # Update driver manager with new passwords
+ update_session_manager(saved_password_servers)
+ current_app.logger.warning('Password migration is successful')
+
+ return passwords_migrated, error
+
+
def reencrpyt_server_passwords(user_id, old_key, new_key):
"""
This function will decrypt the saved passwords in SQLite with old key
@@ -242,7 +401,6 @@ def reencrpyt_server_passwords(user_id, old_key, new_key):
for server in Server.query.filter_by(user_id=user_id).all():
manager = driver.connection_manager(server.id)
-
_password_check(server, manager, old_key, new_key)
if server.tunnel_password is not None:
@@ -274,13 +432,88 @@ def remove_saved_passwords(user_id):
try:
db.session.query(Server) \
.filter(Server.user_id == user_id) \
- .update({Server.password: None, Server.tunnel_password: None})
+ .update({Server.password: None, Server.tunnel_password: None,
+ Server.save_password: 0})
db.session.commit()
except Exception:
db.session.rollback()
raise
+def delete_saved_passwords_from_os_secret_storage(servers):
+ """
+ Delete passwords from os secret storage
+ :param servers: server list
+ :return: True if successful else False
+ """
+ try:
+ # Clears entry created by initial keyring implementation
+ desktop_user_pass = \
+ KEY_RING_DESKTOP_USER.format(current_user.username)
+ if keyring.get_password(KEY_RING_SERVICE_NAME,desktop_user_pass):
+ keyring.delete_password(KEY_RING_SERVICE_NAME, desktop_user_pass)
+
+ if len(servers) > 0:
+ for server in servers:
+ server_name = KEY_RING_USERNAME_FORMAT.format(server.name,
+ server.id)
+ server_password = keyring.get_password(
+ KEY_RING_SERVICE_NAME, server_name)
+ if server_password:
+ keyring.delete_password(
+ KEY_RING_SERVICE_NAME, server_name)
+ else:
+ setattr(server, 'save_password', 0)
+
+ tunnel_name = KEY_RING_TUNNEL_FORMAT.format(server.name,
+ server.id)
+ tunnel_password = keyring.get_password(
+ KEY_RING_SERVICE_NAME, tunnel_name)
+ if tunnel_password:
+ keyring.delete_password(
+ KEY_RING_SERVICE_NAME, tunnel_name)
+ else:
+ setattr(server, 'tunnel_password', None)
+ return True
+ else:
+ # This means no server password to migrate
+ return False
+ except Exception as e:
+ current_app.logger.warning(
+ 'Failed to delete passwords stored in OS password manager.'
+ 'Error: {0}'.format(e))
+ return False
+
+
+def update_session_manager(user_id=None, servers=None):
+ """
+ Updates the passwords in the session
+ :param user_id:
+ :param servers:
+ :return:
+ """
+ from pgadmin.model import Server
+ from pgadmin.utils.driver import get_driver
+ driver = get_driver(config.PG_DEFAULT_DRIVER)
+ try:
+ if user_id:
+ for server in Server.query.\
+ filter_by(user_id=current_user.id).all():
+ manager = driver.connection_manager(server.id)
+ manager.update(server)
+ elif servers:
+ for server in servers:
+ manager = driver.connection_manager(server.id)
+ manager.update(server)
+ else:
+ return False
+ db.session.commit()
+ return True
+ except Exception:
+ db.session.rollback()
+ raise
+
+
def get_replication_type(conn, sversion):
status, res = conn.execute_dict(render_template(
"/servers/sql/#{0}#/replication_type.sql".format(sversion)
diff --git a/web/pgadmin/browser/tests/test_master_password.py b/web/pgadmin/browser/tests/test_master_password.py
index 0834aa5f8..2441e47a0 100644
--- a/web/pgadmin/browser/tests/test_master_password.py
+++ b/web/pgadmin/browser/tests/test_master_password.py
@@ -45,6 +45,7 @@ class MasterPasswordTestCase(BaseTestGenerator):
def setUp(self):
config.MASTER_PASSWORD_REQUIRED = True
config.AUTHENTICATION_SOURCES = [INTERNAL]
+ config.USE_OS_SECRET_STORAGE = False
def runTest(self):
"""This function will check change password functionality."""
diff --git a/web/pgadmin/evaluate_config.py b/web/pgadmin/evaluate_config.py
index e035a899a..54475eac6 100644
--- a/web/pgadmin/evaluate_config.py
+++ b/web/pgadmin/evaluate_config.py
@@ -116,15 +116,15 @@ def evaluate_and_patch_config(config: dict) -> dict:
config['ENABLE_PSQL'] = True
if config.get('SERVER_MODE'):
- config.setdefault('DISABLED_LOCAL_PASSWORD_STORAGE', True)
+ config.setdefault('USE_OS_SECRET_STORAGE', False)
config.setdefault('KEYRING_NAME', '')
else:
k_name = keyring.get_keyring().name
+ # Setup USE_OS_SECRET_STORAGE false as no keyring backend available
if k_name == 'fail Keyring':
- config.setdefault('DISABLED_LOCAL_PASSWORD_STORAGE', True)
- config.setdefault('KEYRING_NAME', '')
+ config['USE_OS_SECRET_STORAGE'] = False
+ config['KEYRING_NAME'] = ''
else:
- config.setdefault('DISABLED_LOCAL_PASSWORD_STORAGE', False)
config.setdefault('KEYRING_NAME', k_name)
config.setdefault('SESSION_COOKIE_PATH', config.get('COOKIE_DEFAULT_PATH'))
diff --git a/web/pgadmin/static/js/Dialogs/MasterPasswordContent.jsx b/web/pgadmin/static/js/Dialogs/MasterPasswordContent.jsx
index 8c62d2dc1..d41d664af 100644
--- a/web/pgadmin/static/js/Dialogs/MasterPasswordContent.jsx
+++ b/web/pgadmin/static/js/Dialogs/MasterPasswordContent.jsx
@@ -64,7 +64,7 @@ export default function MasterPasswordContent({ closeModal, onResetPassowrd, onO
-
+
diff --git a/web/pgadmin/static/js/Dialogs/index.jsx b/web/pgadmin/static/js/Dialogs/index.jsx
index 81e17042f..a3a12451d 100644
--- a/web/pgadmin/static/js/Dialogs/index.jsx
+++ b/web/pgadmin/static/js/Dialogs/index.jsx
@@ -81,39 +81,20 @@ export function checkMasterPassword(data, masterpass_callback_queue, cancel_call
const api = getApiInstance();
api.post(url_for('browser.set_master_password'), data).then((res)=> {
let isKeyring = res.data.data.keyring_name.length > 0;
+ let error = res.data.data.errmsg;
if(!res.data.data.present) {
- if (res.data.data.invalid_master_password_hook){
- if(res.data.data.is_error){
- pgAdmin.Browser.notifier.error(res.data.data.errmsg);
- }else{
- pgAdmin.Browser.notifier.confirm(gettext('Reset Master Password'),
- gettext('The master password retrieved from the master password hook utility is different from what was previously retrieved.') + '
'
- + gettext('Do you want to reset your master password to match?') + '
'
- + gettext('Note that this will close all open database connections and remove all saved passwords.'),
- function() {
- let _url = url_for('browser.reset_master_password');
- api.delete(_url)
- .then(() => {
- pgAdmin.Browser.notifier.info('The master password has been reset.');
- })
- .catch((err) => {
- pgAdmin.Browser.notifier.error(err.message);
- });
- return true;
- },
- function() {/* If user clicks No */ return true;}
- );}
- }else{
- showMasterPassword(res.data.data.reset, res.data.data.errmsg, masterpass_callback_queue, cancel_callback, res.data.data.keyring_name);
- }
-
+ showMasterPassword(res.data.data.reset, res.data.data.errmsg, masterpass_callback_queue, cancel_callback, res.data.data.keyring_name, res.data.data.master_password_hook);
} else {
masterPassCallbacks(masterpass_callback_queue);
-
if(isKeyring) {
- pgAdmin.Browser.notifier.alert(gettext('Migration successful'),
- gettext(`Passwords previously saved by pgAdmin have been successfully migrated to ${res.data.data.keyring_name} and removed from the pgAdmin store.`));
+ if(error){
+ pgAdmin.Browser.notifier.alert(gettext('Migration failed'),
+ gettext(`Passwords previously saved can not be re-encrypted using encryption key stored in the ${res.data.data.keyring_name}. due to ${error}`));
+ }else{
+ pgAdmin.Browser.notifier.alert(gettext('Migration successful'),
+ gettext(`Passwords previously saved are re-encrypted using encryption key stored in the ${res.data.data.keyring_name}.`));
+ }
}
}
}).catch(function(error) {
@@ -122,7 +103,7 @@ export function checkMasterPassword(data, masterpass_callback_queue, cancel_call
}
// This functions is used to show the master password dialog.
-export function showMasterPassword(isPWDPresent, errmsg, masterpass_callback_queue, cancel_callback, keyring_name='') {
+export function showMasterPassword(isPWDPresent, errmsg, masterpass_callback_queue, cancel_callback, keyring_name='', master_password_hook='') {
const api = getApiInstance();
let title = gettext('Set Master Password');
if (keyring_name.length > 0)
@@ -130,47 +111,73 @@ export function showMasterPassword(isPWDPresent, errmsg, masterpass_callback_que
else if (isPWDPresent)
title = gettext('Unlock Saved Passwords');
- pgAdmin.Browser.notifier.showModal(title, (onClose)=> {
- return (
- {
- onClose();
- }}
- onResetPassowrd={(isKeyRing=false)=>{
- pgAdmin.Browser.notifier.confirm(gettext('Reset Master Password'),
- gettext('This will remove all the saved passwords. This will also remove established connections to '
- + 'the server and you may need to reconnect again. Do you wish to continue?'),
- function() {
- let _url = url_for('browser.reset_master_password');
+ if(master_password_hook){
+ if(errmsg){
+ pgAdmin.Browser.notifier.error(errmsg);
+ return true;
+ }else{
+ pgAdmin.Browser.notifier.confirm(gettext('Reset Master Password'),
+ gettext('The master password retrieved from the master password hook utility is different from what was previously retrieved.') + '
'
+ + gettext('Do you want to reset your master password to match?') + '
'
+ + gettext('Note that this will close all open database connections and remove all saved passwords.'),
+ function() {
+ let _url = url_for('browser.reset_master_password');
+ const api = getApiInstance();
+ api.delete(_url)
+ .then(() => {
+ pgAdmin.Browser.notifier.info('The master password has been reset.');
+ })
+ .catch((err) => {
+ pgAdmin.Browser.notifier.error(err.message);
+ });
+ return true;
+ },
+ function() {/* If user clicks No */ return true;}
+ );}
+ }else{
- api.delete(_url)
- .then(() => {
- onClose();
- if(!isKeyRing) {
- showMasterPassword(false, null, masterpass_callback_queue, cancel_callback);
- }
- })
- .catch((err) => {
- pgAdmin.Browser.notifier.error(err.message);
- });
- return true;
- },
- function() {/* If user clicks No */ return true;}
- );
- }}
- onCancel={()=>{
- cancel_callback?.();
- }}
- onOK={(formData) => {
- onClose();
- checkMasterPassword(formData, masterpass_callback_queue, cancel_callback);
- }}
- />
- );
- });
+ pgAdmin.Browser.notifier.showModal(title, (onClose)=> {
+ return (
+ {
+ onClose();
+ }}
+ onResetPassowrd={(isKeyRing=false)=>{
+ pgAdmin.Browser.notifier.confirm(gettext('Reset Master Password'),
+ gettext('This will remove all the saved passwords. This will also remove established connections to '
+ + 'the server and you may need to reconnect again. Do you wish to continue?'),
+ function() {
+ let _url = url_for('browser.reset_master_password');
+
+ api.delete(_url)
+ .then(() => {
+ onClose();
+ if(!isKeyRing) {
+ showMasterPassword(false, null, masterpass_callback_queue, cancel_callback);
+ }
+ })
+ .catch((err) => {
+ pgAdmin.Browser.notifier.error(err.message);
+ });
+ return true;
+ },
+ function() {/* If user clicks No */ return true;}
+ );
+ }}
+ onCancel={()=>{
+ cancel_callback?.();
+ }}
+ onOK={(formData) => {
+ onClose();
+ checkMasterPassword(formData, masterpass_callback_queue, cancel_callback);
+ }}
+ />
+ );
+ });
+ }
}
export function showChangeServerPassword() {
diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py
index 012b42211..f5e5672dd 100644
--- a/web/pgadmin/utils/constants.py
+++ b/web/pgadmin/utils/constants.py
@@ -134,6 +134,7 @@ ACCESS_DENIED_MESSAGE = gettext(
KEY_RING_SERVICE_NAME = 'pgAdmin4'
+KEY_RING_USER_NAME = 'pgadmin4-master-password'
KEY_RING_USERNAME_FORMAT = KEY_RING_SERVICE_NAME + '-{0}-{1}'
KEY_RING_TUNNEL_FORMAT = KEY_RING_SERVICE_NAME + '-tunnel-{0}-{1}'
KEY_RING_DESKTOP_USER = KEY_RING_SERVICE_NAME + '-desktop-user-{0}'
diff --git a/web/pgadmin/utils/driver/psycopg3/connection.py b/web/pgadmin/utils/driver/psycopg3/connection.py
index 11a1865a9..a81c277b3 100644
--- a/web/pgadmin/utils/driver/psycopg3/connection.py
+++ b/web/pgadmin/utils/driver/psycopg3/connection.py
@@ -268,18 +268,11 @@ class Connection(BaseConnection):
manager = self.manager
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- crypt_key_present, crypt_key = get_crypt_key()
-
- if not crypt_key_present:
- raise CryptKeyMissing()
-
- password, encpass, is_update_password = self._check_user_password(
- kwargs)
- else:
- password = None
- encpass = kwargs['password'] if 'password' in kwargs else None
- is_update_password = True
+ crypt_key_present, crypt_key = get_crypt_key()
+ if not crypt_key_present:
+ raise CryptKeyMissing()
+ password, encpass, is_update_password = \
+ self._check_user_password(kwargs)
passfile = kwargs['passfile'] if 'passfile' in kwargs else None
tunnel_password = kwargs['tunnel_password'] if 'tunnel_password' in \
@@ -305,15 +298,13 @@ class Connection(BaseConnection):
if self.reconnecting is not False:
self.password = None
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- is_error, errmsg, password = self._decode_password(encpass,
- manager,
- password,
- crypt_key)
- if is_error:
- return False, errmsg
- else:
- password = encpass
+ if not crypt_key_present:
+ raise CryptKeyMissing()
+
+ is_error, errmsg, password = self._decode_password(
+ encpass, manager, password, crypt_key)
+ if is_error:
+ return False, errmsg
# If no password credential is found then connect request might
# come from Query tool, ViewData grid, debugger etc tools.
@@ -1580,13 +1571,11 @@ Failed to reset the connection to the server due to following error:
user = User.query.filter_by(id=current_user.id).first()
if user is None:
return False, self.UNAUTHORIZED_REQUEST
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- crypt_key_present, crypt_key = get_crypt_key()
- if not crypt_key_present:
- return False, crypt_key
- password = decrypt(password, crypt_key)\
- .decode()
+ crypt_key_present, crypt_key = get_crypt_key()
+ if not crypt_key_present:
+ return False, crypt_key
+ password = decrypt(password, crypt_key).decode()
try:
with ConnectionLocker(self.manager.kerberos_conn):
diff --git a/web/pgadmin/utils/driver/psycopg3/server_manager.py b/web/pgadmin/utils/driver/psycopg3/server_manager.py
index bdedfdffd..5659f6790 100644
--- a/web/pgadmin/utils/driver/psycopg3/server_manager.py
+++ b/web/pgadmin/utils/driver/psycopg3/server_manager.py
@@ -242,8 +242,7 @@ WHERE db.oid = {0}""".format(did))
"Could not find the specified database."
))
- if not get_crypt_key()[0] and (
- config.SERVER_MODE or config.DISABLED_LOCAL_PASSWORD_STORAGE):
+ if not get_crypt_key()[0]:
# the reason its not connected might be missing key
raise CryptKeyMissing()
@@ -537,16 +536,10 @@ WHERE db.oid = {0}""".format(did))
def export_password_env(self, env):
if self.password:
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- crypt_key_present, crypt_key = get_crypt_key()
- if not crypt_key_present:
- return False, crypt_key
- password = decrypt(self.password, crypt_key).decode()
- elif hasattr(self.password, 'decode'):
- password = self.password.decode('utf-8')
- else:
- password = self.password
-
+ crypt_key_present, crypt_key = get_crypt_key()
+ if not crypt_key_present:
+ return False, crypt_key
+ password = decrypt(self.password, crypt_key).decode()
os.environ[str(env)] = password
elif self.passexec:
password = self.passexec.get()
@@ -565,18 +558,15 @@ WHERE db.oid = {0}""".format(did))
return False, gettext("Unauthorized request.")
if tunnel_password is not None and tunnel_password != '':
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- crypt_key_present, crypt_key = get_crypt_key()
- if not crypt_key_present:
- raise CryptKeyMissing()
+ crypt_key_present, crypt_key = get_crypt_key()
+ if not crypt_key_present:
+ raise CryptKeyMissing()
try:
- if config.DISABLED_LOCAL_PASSWORD_STORAGE:
- tunnel_password = decrypt(tunnel_password, crypt_key)
- # password is in bytes, for python3 we need it in string
- if isinstance(tunnel_password, bytes):
- tunnel_password = tunnel_password.decode()
-
+ tunnel_password = decrypt(tunnel_password, crypt_key)
+ # password is in bytes, for python3 we need it in string
+ if isinstance(tunnel_password, bytes):
+ tunnel_password = tunnel_password.decode()
except Exception as e:
current_app.logger.exception(e)
return False, gettext("Failed to decrypt the SSH tunnel "
diff --git a/web/pgadmin/utils/master_password.py b/web/pgadmin/utils/master_password.py
index 0a94c7371..3ebc5ea28 100644
--- a/web/pgadmin/utils/master_password.py
+++ b/web/pgadmin/utils/master_password.py
@@ -1,10 +1,15 @@
+import secrets
+
+import keyring
+from keyring.errors import KeyringError, KeyringLocked, NoKeyringError
+
import config
-from flask import current_app, session, current_app
+from flask import current_app
from flask_login import current_user
from pgadmin.model import db, User, Server
+from pgadmin.utils.constants import KEY_RING_SERVICE_NAME, KEY_RING_USER_NAME
from pgadmin.utils.crypto import encrypt, decrypt
-
MASTERPASS_CHECK_TEXT = 'ideas are bulletproof'
@@ -23,29 +28,41 @@ def get_crypt_key():
:return: the key
"""
enc_key = current_app.keyManager.get()
-
# if desktop mode and master pass disabled then use the password hash
- if not config.MASTER_PASSWORD_REQUIRED \
- and not config.SERVER_MODE:
+ if not config.MASTER_PASSWORD_REQUIRED and\
+ not config.USE_OS_SECRET_STORAGE and not config.SERVER_MODE:
return True, current_user.password
# if desktop mode and master pass enabled
elif config.MASTER_PASSWORD_REQUIRED and \
- config.MASTER_PASSWORD_HOOK is None\
- and enc_key is None:
+ enc_key is None:
return False, None
- elif not config.MASTER_PASSWORD_REQUIRED and config.SERVER_MODE and \
- 'pass_enc_key' in session:
- return True, session['pass_enc_key']
- elif config.MASTER_PASSWORD_REQUIRED and config.SERVER_MODE and \
- config.MASTER_PASSWORD_HOOK and current_user.password is None:
- cmd = config.MASTER_PASSWORD_HOOK
- command = cmd.replace('%u', current_user.username) \
- if '%u' in cmd else cmd
- return get_master_password_from_master_hook(command)
else:
return True, enc_key
+def get_master_password_key_from_os_secret():
+ master_key = None
+ try:
+ # Try to get master key is from local os storage
+ master_key = keyring.get_password(
+ KEY_RING_SERVICE_NAME, KEY_RING_USER_NAME)
+ except KeyringLocked as e:
+ current_app.logger.warning(
+ 'Failed to retrieve master key because Access Denied.'
+ ' Error: {0}'.format(e))
+ config.USE_OS_SECRET_STORAGE = False
+ except Exception as e:
+ current_app.logger.warning(
+ 'Failed to set encryption key using OS password manager'
+ ', fallback to master password. Error: {0}'.format(e))
+ config.USE_OS_SECRET_STORAGE = False
+ return master_key
+
+
+def generate_master_password_key_for_os_secret():
+ return secrets.token_urlsafe(12)
+
+
def validate_master_password(password):
"""
Validate the password/key against the stored encrypted text
@@ -114,6 +131,47 @@ def cleanup_master_password():
manager.update(server)
+def delete_local_storage_master_key():
+ """
+ Deletes the auto generated master key stored in keyring
+ """
+ if not config.SERVER_MODE and not config.USE_OS_SECRET_STORAGE:
+ # Retrieve from os secret storage
+ try:
+ # try to get key
+ master_key = keyring.get_password(KEY_RING_SERVICE_NAME,
+ KEY_RING_USER_NAME)
+ if master_key:
+ keyring.delete_password(KEY_RING_SERVICE_NAME,
+ KEY_RING_USER_NAME)
+ from pgadmin.browser.server_groups.servers.utils \
+ import remove_saved_passwords
+ remove_saved_passwords(current_user.id)
+
+ from pgadmin.utils.driver import get_driver
+ driver = get_driver(config.PG_DEFAULT_DRIVER)
+ for server in Server.query.filter_by(
+ user_id=current_user.id).all():
+ manager = driver.connection_manager(server.id)
+ manager.update(server)
+ current_app.logger.warning(
+ 'Deleted master key stored in OS password manager.')
+ except NoKeyringError as e:
+ current_app.logger.warning(
+ ' Failed to delete master key stored in OS password manager'
+ ' because Keyring backend not found. Error: {0}'.format(e))
+ config.USE_OS_SECRET_STORAGE = False
+ except KeyringLocked as e:
+ current_app.logger.warning(
+ ' Failed to delete master key stored in OS password manager'
+ ' because of Access Denied. Error: {0}'.format(e))
+ config.USE_OS_SECRET_STORAGE = False
+ except Exception as e:
+ current_app.logger.warning(
+ 'Failed to delete master key stored in OS password manager.')
+ config.USE_OS_SECRET_STORAGE = False
+
+
def process_masterpass_disabled():
"""
On master password disable, remove the connection data from session as it
@@ -129,28 +187,30 @@ def process_masterpass_disabled():
return False
-def get_master_password_from_master_hook(command):
+def get_master_password_from_master_hook():
"""
This method executes specified command & returns output.
:param command: Shell command with absolute path
:return: Output of command.
"""
import subprocess
+ cmd = config.MASTER_PASSWORD_HOOK
+ command = cmd.replace('%u', current_user.username) \
+ if '%u' in cmd else cmd
try:
p = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
out, err = p.communicate()
if p.returncode == 0:
output = out.decode() if hasattr(out, 'decode') else out
output = output.strip()
- return True, output
+ return output
else:
error = "Command '{0}' failed, exit-code={1} error = {2}".format(
command, p.returncode, str(err))
current_app.logger.error(error)
- return False, None
except Exception as e:
current_app.logger.exception(
'Failed to retrieve master password from the master password hook'
' utility.Error: {0}'.format(e)
)
- return False, None
+ return None
diff --git a/web/regression/runtests.py b/web/regression/runtests.py
index cf6c3a7be..7aecc0417 100644
--- a/web/regression/runtests.py
+++ b/web/regression/runtests.py
@@ -60,6 +60,7 @@ if config.SERVER_MODE is True:
# disable master password for test cases
config.MASTER_PASSWORD_REQUIRED = False
+config.USE_OS_SECRET_STORAGE = False
from regression import test_setup
from regression.feature_utils.app_starter import AppStarter