def decrypt_pdf(filename: str = None, passphrase: str = None, verbose: bool = False) -> str: assert (filename is not None), '<filename> must not be None' assert (passphrase is not None), '<passphrase> must not be None' gmLog2.add_word2hide(passphrase) _log.debug('attempting PDF decryption') for cmd in ['qpdf', 'qpdf.exe']: found, binary = gmShellAPI.detect_external_binary(binary=cmd) if found: break if not found: _log.warning('no qpdf binary found') return None filename_decrypted = '%s.decrypted.pdf' % os.path.splitext(filename)[0] args = [ binary, '--verbose', '--password-mode=unicode', '--decrypt', '--password=%s' % passphrase, '--', filename, filename_decrypted ] success, exit_code, stdout = gmShellAPI.run_process( cmd_line=args, encoding='utf8', verbose=verbose, acceptable_return_codes=[0, 3]) if not success: return None return filename_decrypted
def encrypt_pdf(filename=None, passphrase=None, verbose=False): assert (filename is not None), '<filename> must not be None' assert (passphrase is not None), '<passphrase> must not be None' if len(passphrase) < 5: _log.error('<passphrase> must be at least 5 characters/signs/digits') return None gmLog2.add_word2hide(passphrase) _log.debug('attempting PDF encryption') for cmd in ['qpdf', 'qpdf.exe']: found, binary = gmShellAPI.detect_external_binary(binary = cmd) if found: break if not found: _log.warning('no qpdf binary found') return None filename_encrypted = '%s.encrypted.pdf' % os.path.splitext(filename)[0] args = [ binary, '--verbose', '--encrypt', passphrase, '', '128', '--print=full', '--modify=none', '--extract=n', '--use-aes=y', '--', filename, filename_encrypted ] success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose) if success: return filename_encrypted return None
def __get_password(self, msg_title): while True: data_pwd = wx.GetPasswordFromUser ( message = _( 'Enter passphrase to protect the data with.\n' '\n' '(minimum length: 5, trailing blanks will be stripped)' ), caption = msg_title ) # minimal weakness check data_pwd = data_pwd.rstrip() if len(data_pwd) > 4: break retry = gmGuiHelpers.gm_show_question ( title = msg_title, question = _( 'Insufficient passphrase.\n' '\n' '(minimum length: 5, trailing blanks will be stripped)\n' '\n' 'Enter another passphrase ?' ) ) if not retry: # user changed her mind return None # confidentiality gmLog2.add_word2hide(data_pwd) # reget password while True: data_pwd4comparison = wx.GetPasswordFromUser ( message = _( 'Once more enter passphrase to protect the data with.\n' '\n' '(this will protect you from typos)\n' '\n' 'Abort by leaving empty.' ), caption = msg_title ) data_pwd4comparison = data_pwd4comparison.rstrip() if data_pwd4comparison == '': # user changed her mind ... return None if data_pwd == data_pwd4comparison: break gmGuiHelpers.gm_show_error ( error = _( 'Passphrases do not match.\n' '\n' 'Retry, or abort with an empty passphrase.' ), title = msg_title ) return data_pwd
def aes_encrypt_file(filename=None, passphrase=None, comment=None, verbose=False, remove_unencrypted=False): assert (filename is not None), '<filename> must not be None' assert (passphrase is not None), '<passphrase> must not be None' if len(passphrase) < 5: _log.error('<passphrase> must be at least 5 characters/signs/digits') return None gmLog2.add_word2hide(passphrase) #add 7z/winzip url to comment.txt _log.debug('attempting 7z AES encryption') for cmd in ['7z', '7z.exe']: found, binary = gmShellAPI.detect_external_binary(binary=cmd) if found: break if not found: _log.warning('no 7z binary found, trying gpg') return None if comment is not None: archive_path, archive_name = os.path.split(os.path.abspath(filename)) comment_filename = gmTools.get_unique_filename( prefix='%s.7z.comment-' % archive_name, tmp_dir=archive_path, suffix='.txt') with open(comment_filename, mode='wt', encoding='utf8', errors='replace') as comment_file: comment_file.write(comment) else: comment_filename = '' filename_encrypted = '%s.7z' % filename args = [ binary, 'a', '-bb3', '-mx0', "-p%s" % passphrase, filename_encrypted, filename, comment_filename ] encrypted, exit_code, stdout = gmShellAPI.run_process(cmd_line=args, encoding='utf8', verbose=verbose) gmTools.remove_file(comment_filename) if not encrypted: return None if not remove_unencrypted: return filename_encrypted if gmTools.remove_file(filename): return filename_encrypted gmTools.remove_file(filename_encrypted) return None
def encrypt_pdf(filename: str = None, passphrase: str = None, verbose: bool = False, remove_unencrypted: bool = False) -> str: """Encrypt a PDF file per spec (AES, that is). Args: filename: PDF file to encrypt passphrase: minimum of 5 characters remove_unencrypted: remove unencrypted source file if encryption succeeds Returns: Name of encrypted PDF or None. """ assert (filename is not None), '<filename> must not be None' assert (passphrase is not None), '<passphrase> must not be None' if len(passphrase) < 5: _log.error('<passphrase> must be at least 5 characters/signs/digits') return None gmLog2.add_word2hide(passphrase) _log.debug('attempting PDF encryption') for cmd in ['qpdf', 'qpdf.exe']: found, binary = gmShellAPI.detect_external_binary(binary=cmd) if found: break if not found: _log.warning('no qpdf binary found') return None filename_encrypted = '%s.encrypted.pdf' % os.path.splitext(filename)[0] args = [ binary, '--verbose', '--password-mode=unicode', '--encrypt', passphrase, '', '128', '--print=full', '--modify=none', '--extract=n', '--use-aes=y', '--', filename, filename_encrypted ] success, exit_code, stdout = gmShellAPI.run_process( cmd_line=args, encoding='utf8', verbose=verbose, acceptable_return_codes=[0, 3]) if not success: return None if not remove_unencrypted: return filename_encrypted if gmTools.remove_file(filename): return filename_encrypted gmTools.remove_file(filename_encrypted) return None
def get_dbowner_connection(procedure=None, dbo_password=None, dbo_account='gm-dbo'): if procedure is None: procedure = _('<restricted procedure>') # 1) get password for gm-dbo if dbo_password is None: dbo_password = wx.GetPasswordFromUser ( message = _(""" [%s] This is a restricted procedure. We need the current password for the GNUmed database owner. Please enter the current password for <%s>:""") % ( procedure, dbo_account ), caption = procedure ) if dbo_password == '': return None gmLog2.add_word2hide(dbo_password) # 2) connect as gm-dbo login = gmPG2.get_default_login() dsn = gmPG2.make_psycopg2_dsn ( database = login.database, host = login.host, port = login.port, user = dbo_account, password = dbo_password ) try: conn = gmPG2.get_connection ( dsn = dsn, readonly = False, verbose = True, pooled = False ) except: _log.exception('cannot connect') gmGuiHelpers.gm_show_error ( aMessage = _('Cannot connect as the GNUmed database owner <%s>.') % dbo_account, aTitle = procedure ) gmPG2.log_database_access(action = 'failed to connect as database owner for [%s]' % procedure) return None return conn
def get_dbowner_connection(procedure=None, dbo_password=None, dbo_account='gm-dbo'): if procedure is None: procedure = _('<restricted procedure>') # 1) get password for gm-dbo if dbo_password is None: dbo_password = wx.GetPasswordFromUser(message=_(""" [%s] This is a restricted procedure. We need the current password for the GNUmed database owner. Please enter the current password for <%s>:""") % (procedure, dbo_account), caption=procedure) if dbo_password == '': return None gmLog2.add_word2hide(dbo_password) # 2) connect as gm-dbo login = gmPG2.get_default_login() dsn = gmPG2.make_psycopg2_dsn(database=login.database, host=login.host, port=login.port, user=dbo_account, password=dbo_password) try: conn = gmPG2.get_connection(dsn=dsn, readonly=False, verbose=True, pooled=False) except Exception: _log.exception('cannot connect') gmGuiHelpers.gm_show_error( aMessage=_('Cannot connect as the GNUmed database owner <%s>.') % dbo_account, aTitle=procedure) gmPG2.log_database_access( action='failed to connect as database owner for [%s]' % procedure) return None return conn
def aes_encrypt_file(filename=None, passphrase=None, comment=None, verbose=False, remove_unencrypted=False): assert (filename is not None), '<filename> must not be None' assert (passphrase is not None), '<passphrase> must not be None' if len(passphrase) < 5: _log.error('<passphrase> must be at least 5 characters/signs/digits') return None gmLog2.add_word2hide(passphrase) #add 7z/winzip url to comment.txt _log.debug('attempting 7z AES encryption') for cmd in ['7z', '7z.exe']: found, binary = gmShellAPI.detect_external_binary(binary = cmd) if found: break if not found: _log.warning('no 7z binary found, trying gpg') return None if comment is not None: archive_path, archive_name = os.path.split(os.path.abspath(filename)) comment_filename = gmTools.get_unique_filename ( prefix = '%s.7z.comment-' % archive_name, tmp_dir = archive_path, suffix = '.txt' ) with open(comment_filename, mode = 'wt', encoding = 'utf8', errors = 'replace') as comment_file: comment_file.write(comment) else: comment_filename = '' filename_encrypted = '%s.7z' % filename args = [binary, 'a', '-bb3', '-mx0', "-p%s" % passphrase, filename_encrypted, filename, comment_filename] encrypted, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose) gmTools.remove_file(comment_filename) if not encrypted: return None if not remove_unencrypted: return filename_encrypted if gmTools.remove_file(filename): return filename_encrypted gmTools.remove_file(filename_encrypted) return None
def create_encrypted_zip_archive_from_dir(source_dir: str, comment: str = None, overwrite: bool = True, passphrase: str = None, verbose: bool = False) -> bool: """Create encrypted archive of a directory. The encrypted archive file will always be named "datawrapper.zip" for confidentiality reasons. If callers want another name they will have to shutil.move() the zip file themselves. This archive will be compressed and AES256 encrypted with the given passphrase. Therefore, the result will not decrypt with earlier versions of unzip software. On Windows, 7z oder WinZip are needed. The zip format does not support header encryption thereby allowing attackers to gain knowledge of patient details by observing the names of files and directories inside the encrypted archive. To reduce that attack surface, GNUmed will create _another_ zip archive inside "datawrapper.zip", which eventually wraps up the patient data as "data.zip". That archive is not compressed and not encrypted, and can thus be unpacked with any old unzipper. Note that GNUmed does NOT remember the passphrase for you. You will have to take care of that yourself, and possibly also safely hand over the passphrase to any receivers of the zip archive. Args: source_dir: the directory to archive and encrypt comment: included as a file containing the comment overwrite: remove preexisting archive before creation, avoiding *updating* of same, and thereby including unintended data passphrase: minimum length of 5 if given Returns: True / False / None (other error) """ assert (source_dir is not None), '<source_dir> must not be <None>' if len(passphrase) < 5: _log.error('<passphrase> must be at least 5 characters/signs/digits') return None gmLog2.add_word2hide(passphrase) source_dir = os.path.abspath(source_dir) if not os.path.isdir(source_dir): _log.error('<source_dir> does not exist or is not a directory: %s', source_dir) return False for cmd in ['7z', '7z.exe']: found, binary = gmShellAPI.detect_external_binary(binary=cmd) if found: break if not found: _log.warning('no 7z binary found') return None sandbox_dir = gmTools.mk_sandbox_dir() archive_path_inner = os.path.join(sandbox_dir, 'data') if not gmTools.mkdir(archive_path_inner): _log.error('cannot create scratch space for inner achive: %s', archive_path_inner) archive_fname_inner = 'data.zip' archive_name_inner = os.path.join(archive_path_inner, archive_fname_inner) archive_path_outer = gmTools.gmPaths().tmp_dir archive_fname_outer = 'datawrapper.zip' archive_name_outer = os.path.join(archive_path_outer, archive_fname_outer) # remove existing archives so they don't get *updated* rather than newly created if overwrite: if not gmTools.remove_file(archive_name_inner, force=True): _log.error('cannot remove existing archive [%s]', archive_name_inner) return False if not gmTools.remove_file(archive_name_outer, force=True): _log.error('cannot remove existing archive [%s]', archive_name_outer) return False # 7z does not support ZIP comments so create a text file holding the comment if comment is not None: tmp, fname = os.path.split(source_dir.rstrip(os.sep)) comment_filename = os.path.join(sandbox_dir, '000-%s-comment.txt' % fname) with open(comment_filename, mode='wt', encoding='utf8', errors='replace') as comment_file: comment_file.write(comment) # create inner (data) archive: uncompressed, unencrypted, similar to a tar archive args = [ binary, 'a', # create archive '-sas', # be smart about archive name extension '-bd', # no progress indicator '-mx0', # no compression (only store files) '-mcu=on', # UTF8 filenames '-l', # store content of links, not links '-scsUTF-8', # console charset '-tzip' # force ZIP format ] if verbose: args.append('-bb3') args.append('-bt') else: args.append('-bb1') args.append(archive_name_inner) args.append(source_dir) if comment is not None: args.append(comment_filename) success, exit_code, stdout = gmShellAPI.run_process(cmd_line=args, encoding='utf8', verbose=verbose) if not success: _log.error('cannot create inner archive') return None # create "decompress instructions" file instructions_filename = os.path.join( archive_path_inner, '000-on_Windows-open_with-WinZip_or_7z_tools') open(instructions_filename, mode='wt').close() # create outer (wrapper) archive: compressed, encrypted args = [ binary, 'a', # create archive '-sas', # be smart about archive name extension '-bd', # no progress indicator '-mx9', # best available zip compression ratio '-mcu=on', # UTF8 filenames '-l', # store content of links, not links '-scsUTF-8', # console charset '-tzip', # force ZIP format '-mem=AES256', # force useful encryption '-p%s' % passphrase # set passphrase ] if verbose: args.append('-bb3') args.append('-bt') else: args.append('-bb1') args.append(archive_name_outer) args.append(archive_path_inner) success, exit_code, stdout = gmShellAPI.run_process(cmd_line=args, encoding='utf8', verbose=verbose) if success: return archive_name_outer _log.error('cannot create outer archive') return None
def change_gmdbowner_password(): title = _('Changing GNUmed database owner password') dbo_account = wx.GetTextFromUser( message=_("Enter the account name of the GNUmed database owner:"), caption=title, default_value='') if dbo_account.strip() == '': return False dbo_conn = get_dbowner_connection(procedure=title, dbo_account=dbo_account) if dbo_conn is None: return False dbo_pwd_new_1 = wx.GetPasswordFromUser( message=_("Enter the NEW password for the GNUmed database owner:"), caption=title) if dbo_pwd_new_1.strip() == '': return False gmLog2.add_word2hide(dbo_pwd_new_1) dbo_pwd_new_2 = wx.GetPasswordFromUser(message=_( """Enter the NEW password for the GNUmed database owner, again. (This will protect you from typos.) """), caption=title) if dbo_pwd_new_2.strip() == '': return False if dbo_pwd_new_1 != dbo_pwd_new_2: return False # pwd2 == pwd1 at this point so no need to hide (again) """ On Mon, Mar 13, 2017 at 12:19:22PM -0400, Tom Lane wrote: > Date: Mon, 13 Mar 2017 12:19:22 -0400 > From: Tom Lane <*****@*****.**> > To: Adrian Klaver <*****@*****.**> > cc: Schmid Andreas <*****@*****.**>, > "'*****@*****.**'" <*****@*****.**> > Subject: Re: [GENERAL] createuser: How to specify a database to connect to > > Adrian Klaver <*****@*****.**> writes: > > On 03/13/2017 08:52 AM, Tom Lane wrote: > >> If by "history" you're worried about the server-side statement log, this > >> is merest fantasy: the createuser program is not magic, it just constructs > >> and sends a CREATE USER command for you. You'd actually be more secure > >> using psql, where (if you're superuser) you could shut off log_statement > >> for your session first. > > > There is a difference though: > > > psql> CREATE USER: > > > postgres-2017-03-13 09:03:27.147 PDT-0LOG: statement: create user > > dummy_user with login password '1234'; > > Well, what you're supposed to do is > > postgres=# create user dummy_user; > postgres=# \password dummy_user > Enter new password: > Enter it again: > postgres=# > > which will result in sending something like > > ALTER USER dummy_user PASSWORD 'md5c5e9567bc40082671d02c654260e0e09' > > You can additionally protect that by wrapping it into one transaction > (if you have a setup where the momentary existence of the role without a > password would be problematic) and/or shutting off logging beforehand. """ # this REALLY should be prefixed with md5 and the md5sum sent rather than the pwd cmd = """ALTER ROLE "%s" ENCRYPTED PASSWORD '%s';""" % (dbo_account, dbo_pwd_new_2) gmPG2.run_rw_queries(link_obj=dbo_conn, queries=[{ 'cmd': cmd }], end_tx=True) return True
def connect_to_database(max_attempts=3, expected_version=None, require_version=True): """Display the login dialog and try to log into the backend. - up to max_attempts times - returns True/False """ # force programmer to set a valid expected_version expected_hash = gmPG2.known_schema_hashes[expected_version] client_version = _cfg.get(option='client_version') global current_db_name current_db_name = 'gnumed_v%s' % expected_version attempt = 0 dlg = cLoginDialog(None, -1, client_version=client_version) dlg.Centre(wx.BOTH) while attempt < max_attempts: _log.debug('login attempt %s of %s', (attempt + 1), max_attempts) connected = False dlg.ShowModal() login = dlg.panel.GetLoginInfo() if login is None: _log.info("user cancelled login dialog") break gmLog2.add_word2hide(login.password) # try getting a connection to verify the DSN works dsn = gmPG2.make_psycopg2_dsn(database=login.database, host=login.host, port=login.port, user=login.user, password=login.password) try: conn = gmPG2.get_raw_connection(dsn=dsn, verbose=True, readonly=True) connected = True except gmPG2.cAuthenticationError as e: attempt += 1 _log.error("login attempt failed: %s", e) if attempt < max_attempts: if ('host=127.0.0.1' in ('%s' % e)) or ('host=' not in ('%s' % e)): msg = _( 'Unable to connect to database:\n\n' '%s\n\n' "Are you sure you have got a local database installed ?\n" '\n' "Please retry with proper credentials or cancel.\n" '\n' ' (for the public and any new GNUmed data-\n' ' bases the default user name and password\n' ' are {any-doc, any-doc})\n' '\n' 'You may also need to check the PostgreSQL client\n' 'authentication configuration in pg_hba.conf. For\n' 'details see:\n' '\n' 'wiki.gnumed.de/bin/view/Gnumed/ConfigurePostgreSQL') else: msg = _( "Unable to connect to database:\n\n" "%s\n\n" "Please retry with proper credentials or cancel.\n" "\n" "For the public and any new GNUmed databases the\n" "default user name and password are {any-doc, any-doc}.\n" "\n" 'You may also need to check the PostgreSQL client\n' 'authentication configuration in pg_hba.conf. For\n' 'details see:\n' '\n' 'wiki.gnumed.de/bin/view/Gnumed/ConfigurePostgreSQL') msg = msg % e msg = regex.sub( r'password=[^\s]+', 'password=%s' % gmTools.u_replacement_character, msg) gmGuiHelpers.gm_show_error(msg, _('Connecting to backend')) del e continue except gmPG2.dbapi.OperationalError as exc: _log.exception('login attempt failed') gmPG2.log_pg_exception_details(exc) msg = _( "Unable to connect to database:\n\n" "%s\n\n" "Please retry another backend / user / password combination !\n" "\n" " (for the public and any new GNUmed databases\n" " the default user name and password are\n" " {any-doc, any-doc})\n" "\n") % exc msg = regex.sub(r'password=[^\s]+', 'password=%s' % gmTools.u_replacement_character, msg) gmGuiHelpers.gm_show_error(msg, _('Connecting to backend')) del exc continue conn.close() # connect was successful gmPG2.set_default_login(login=login) gmPG2.set_default_client_encoding( encoding=dlg.panel.backend_profile.encoding) seems_bootstrapped = gmPG2.schema_exists(schema='gm') if not seems_bootstrapped: _log.error( 'schema [gm] does not exist - database not bootstrapped ?') msg = _('The database you connected to does not seem\n' 'to have been boostrapped properly.\n' '\n' 'Make sure you have run the GNUmed database\n' 'bootstrapper tool to create a new database.\n' '\n' 'Further help can be found on the website at\n' '\n' ' http://wiki.gnumed.de\n' '\n' 'or on the GNUmed mailing list.') gmGuiHelpers.gm_show_error(msg, _('Verifying database')) connected = False break compatible = gmPG2.database_schema_compatible(version=expected_version) if compatible or not require_version: dlg.panel.save_state() if not compatible: connected_db_version = gmPG2.get_schema_version() msg = msg_generic % (client_version, connected_db_version, expected_version, gmTools.coalesce(login.host, '<localhost>'), login.database, login.user) if require_version: gmGuiHelpers.gm_show_error(msg + msg_fail, _('Verifying database version')) connected = False continue gmGuiHelpers.gm_show_info(msg + msg_override, _('Verifying database version')) # FIXME: make configurable max_skew = 1 # minutes if _cfg.get(option='debug'): max_skew = 10 if not gmPG2.sanity_check_time_skew(tolerance=(max_skew * 60)): if _cfg.get(option='debug'): gmGuiHelpers.gm_show_warning(msg_time_skew_warn % max_skew, _('Verifying database settings')) else: gmGuiHelpers.gm_show_error(msg_time_skew_fail % max_skew, _('Verifying database settings')) connected = False continue sanity_level, message = gmPG2.sanity_check_database_settings() if sanity_level != 0: gmGuiHelpers.gm_show_error((msg_insanity % message), _('Verifying database settings')) if sanity_level == 2: connected = False continue gmExceptionHandlingWidgets.set_is_public_database(login.public_db) gmExceptionHandlingWidgets.set_helpdesk(login.helpdesk) conn = gmPG2.get_connection( verbose=True, connection_name='GNUmed-[DbListenerThread]', pooled=False) listener = gmBackendListener.gmBackendListener(conn=conn) break dlg.DestroyLater() return connected
def connect_to_database(max_attempts=3, expected_version=None, require_version=True): """Display the login dialog and try to log into the backend. - up to max_attempts times - returns True/False """ # force programmer to set a valid expected_version expected_hash = gmPG2.known_schema_hashes[expected_version] client_version = _cfg.get(option = 'client_version') global current_db_name current_db_name = 'gnumed_v%s' % expected_version attempt = 0 dlg = cLoginDialog(None, -1, client_version = client_version) dlg.Centre(wx.BOTH) while attempt < max_attempts: _log.debug('login attempt %s of %s', (attempt+1), max_attempts) connected = False dlg.ShowModal() login = dlg.panel.GetLoginInfo() if login is None: _log.info("user cancelled login dialog") break gmLog2.add_word2hide(login.password) # try getting a connection to verify the DSN works dsn = gmPG2.make_psycopg2_dsn ( database = login.database, host = login.host, port = login.port, user = login.user, password = login.password ) try: conn = gmPG2.get_raw_connection(dsn = dsn, verbose = True, readonly = True) connected = True except gmPG2.cAuthenticationError as e: attempt += 1 _log.error("login attempt failed: %s", e) if attempt < max_attempts: if ('host=127.0.0.1' in ('%s' % e)) or ('host=' not in ('%s' % e)): msg = _( 'Unable to connect to database:\n\n' '%s\n\n' "Are you sure you have got a local database installed ?\n" '\n' "Please retry with proper credentials or cancel.\n" '\n' ' (for the public and any new GNUmed data-\n' ' bases the default user name and password\n' ' are {any-doc, any-doc})\n' '\n' 'You may also need to check the PostgreSQL client\n' 'authentication configuration in pg_hba.conf. For\n' 'details see:\n' '\n' 'wiki.gnumed.de/bin/view/Gnumed/ConfigurePostgreSQL' ) else: msg = _( "Unable to connect to database:\n\n" "%s\n\n" "Please retry with proper credentials or cancel.\n" "\n" "For the public and any new GNUmed databases the\n" "default user name and password are {any-doc, any-doc}.\n" "\n" 'You may also need to check the PostgreSQL client\n' 'authentication configuration in pg_hba.conf. For\n' 'details see:\n' '\n' 'wiki.gnumed.de/bin/view/Gnumed/ConfigurePostgreSQL' ) msg = msg % e msg = regex.sub(r'password=[^\s]+', 'password=%s' % gmTools.u_replacement_character, msg) gmGuiHelpers.gm_show_error ( msg, _('Connecting to backend') ) del e continue except gmPG2.dbapi.OperationalError as exc: _log.exception('login attempt failed') gmPG2.log_pg_exception_details(exc) msg = _( "Unable to connect to database:\n\n" "%s\n\n" "Please retry another backend / user / password combination !\n" "\n" " (for the public and any new GNUmed databases\n" " the default user name and password are\n" " {any-doc, any-doc})\n" "\n" ) % exc msg = regex.sub(r'password=[^\s]+', 'password=%s' % gmTools.u_replacement_character, msg) gmGuiHelpers.gm_show_error(msg, _('Connecting to backend')) del exc continue conn.close() # connect was successful gmPG2.set_default_login(login = login) gmPG2.set_default_client_encoding(encoding = dlg.panel.backend_profile.encoding) seems_bootstrapped = gmPG2.schema_exists(schema = 'gm') if not seems_bootstrapped: _log.error('schema [gm] does not exist - database not bootstrapped ?') msg = _( 'The database you connected to does not seem\n' 'to have been boostrapped properly.\n' '\n' 'Make sure you have run the GNUmed database\n' 'bootstrapper tool to create a new database.\n' '\n' 'Further help can be found on the website at\n' '\n' ' http://wiki.gnumed.de\n' '\n' 'or on the GNUmed mailing list.' ) gmGuiHelpers.gm_show_error(msg, _('Verifying database')) connected = False break compatible = gmPG2.database_schema_compatible(version = expected_version) if compatible or not require_version: dlg.panel.save_state() if not compatible: connected_db_version = gmPG2.get_schema_version() msg = msg_generic % ( client_version, connected_db_version, expected_version, gmTools.coalesce(login.host, '<localhost>'), login.database, login.user ) if require_version: gmGuiHelpers.gm_show_error(msg + msg_fail, _('Verifying database version')) connected = False continue gmGuiHelpers.gm_show_info(msg + msg_override, _('Verifying database version')) # FIXME: make configurable max_skew = 1 # minutes if _cfg.get(option = 'debug'): max_skew = 10 if not gmPG2.sanity_check_time_skew(tolerance = (max_skew * 60)): if _cfg.get(option = 'debug'): gmGuiHelpers.gm_show_warning(msg_time_skew_warn % max_skew, _('Verifying database settings')) else: gmGuiHelpers.gm_show_error(msg_time_skew_fail % max_skew, _('Verifying database settings')) connected = False continue sanity_level, message = gmPG2.sanity_check_database_settings() if sanity_level != 0: gmGuiHelpers.gm_show_error((msg_insanity % message), _('Verifying database settings')) if sanity_level == 2: connected = False continue gmExceptionHandlingWidgets.set_is_public_database(login.public_db) gmExceptionHandlingWidgets.set_helpdesk(login.helpdesk) conn = gmPG2.get_connection(verbose = True, connection_name = 'GNUmed-[DbListenerThread]', pooled = False) listener = gmBackendListener.gmBackendListener(conn = conn) break dlg.DestroyLater() return connected
def create_encrypted_zip_archive_from_dir(source_dir, comment=None, overwrite=True, passphrase=None, verbose=False): """Use 7z to create an encrypted ZIP archive of a directory. <source_dir> will be included into the archive <comment> included as a file containing the comment <overwrite> remove existing archive before creation, avoiding *updating* of those, and thereby including unintended data <passphrase> minimum length of 5 The resulting zip archive will always be named "datawrapper.zip" for confidentiality reasons. If callers want another name they will have to shutil.move() the zip file themselves. This archive will be compressed and AES256 encrypted with the given passphrase. Therefore, the result will not decrypt with earlier versions of unzip software. On Windows, 7z oder WinZip are needed. The zip format does not support header encryption thereby allowing attackers to gain knowledge of patient details by observing the names of files and directories inside the encrypted archive. To reduce that attack surface, GNUmed will create _another_ zip archive inside "datawrapper.zip", which eventually wraps up the patient data as "data.zip". That archive is not compressed and not encrypted, and can thus be unpacked with any old unzipper. Note that GNUmed does NOT remember the passphrase for you. You will have to take care of that yourself, and possibly also safely hand over the passphrase to any receivers of the zip archive. """ if len(passphrase) < 5: _log.error('<passphrase> must be at least 5 characters/signs/digits') return None gmLog2.add_word2hide(passphrase) source_dir = os.path.abspath(source_dir) if not os.path.isdir(source_dir): _log.error('<source_dir> does not exist or is not a directory: %s', source_dir) return False for cmd in ['7z', '7z.exe']: found, binary = gmShellAPI.detect_external_binary(binary = cmd) if found: break if not found: _log.warning('no 7z binary found') return None sandbox_dir = gmTools.mk_sandbox_dir() archive_path_inner = os.path.join(sandbox_dir, 'data') if not gmTools.mkdir(archive_path_inner): _log.error('cannot create scratch space for inner achive: %s', archive_path_inner) archive_fname_inner = 'data.zip' archive_name_inner = os.path.join(archive_path_inner, archive_fname_inner) archive_path_outer = gmTools.gmPaths().tmp_dir archive_fname_outer = 'datawrapper.zip' archive_name_outer = os.path.join(archive_path_outer, archive_fname_outer) # remove existing archives so they don't get *updated* rather than newly created if overwrite: if not gmTools.remove_file(archive_name_inner, force = True): _log.error('cannot remove existing archive [%s]', archive_name_inner) return False if not gmTools.remove_file(archive_name_outer, force = True): _log.error('cannot remove existing archive [%s]', archive_name_outer) return False # 7z does not support ZIP comments so create a text file holding the comment if comment is not None: tmp, fname = os.path.split(source_dir.rstrip(os.sep)) comment_filename = os.path.join(sandbox_dir, '000-%s-comment.txt' % fname) with open(comment_filename, mode = 'wt', encoding = 'utf8', errors = 'replace') as comment_file: comment_file.write(comment) # create inner (data) archive: uncompressed, unencrypted, similar to a tar archive args = [ binary, 'a', # create archive '-sas', # be smart about archive name extension '-bd', # no progress indicator '-mx0', # no compression (only store files) '-mcu=on', # UTF8 filenames '-l', # store content of links, not links '-scsUTF-8', # console charset '-tzip' # force ZIP format ] if verbose: args.append('-bb3') args.append('-bt') else: args.append('-bb1') args.append(archive_name_inner) args.append(source_dir) if comment is not None: args.append(comment_filename) success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose) if not success: _log.error('cannot create inner archive') return None # create "decompress instructions" file instructions_filename = os.path.join(archive_path_inner, '000-on_Windows-open_with-WinZip_or_7z_tools') open(instructions_filename, mode = 'wt').close() # create outer (wrapper) archive: compressed, encrypted args = [ binary, 'a', # create archive '-sas', # be smart about archive name extension '-bd', # no progress indicator '-mx9', # best available zip compression ratio '-mcu=on', # UTF8 filenames '-l', # store content of links, not links '-scsUTF-8', # console charset '-tzip', # force ZIP format '-mem=AES256', # force useful encryption '-p%s' % passphrase # set passphrase ] if verbose: args.append('-bb3') args.append('-bt') else: args.append('-bb1') args.append(archive_name_outer) args.append(archive_path_inner) success, exit_code, stdout = gmShellAPI.run_process(cmd_line = args, encoding = 'utf8', verbose = verbose) if success: return archive_name_outer _log.error('cannot create outer archive') return None
def change_gmdbowner_password(): title = _('Changing GNUmed database owner password') dbo_account = wx.GetTextFromUser ( message = _("Enter the account name of the GNUmed database owner:"), caption = title, default_value = '' ) if dbo_account.strip() == '': return False dbo_conn = get_dbowner_connection ( procedure = title, dbo_account = dbo_account ) if dbo_conn is None: return False dbo_pwd_new_1 = wx.GetPasswordFromUser ( message = _("Enter the NEW password for the GNUmed database owner:"), caption = title ) if dbo_pwd_new_1.strip() == '': return False gmLog2.add_word2hide(dbo_pwd_new_1) dbo_pwd_new_2 = wx.GetPasswordFromUser ( message = _("""Enter the NEW password for the GNUmed database owner, again. (This will protect you from typos.) """), caption = title ) if dbo_pwd_new_2.strip() == '': return False if dbo_pwd_new_1 != dbo_pwd_new_2: return False # pwd2 == pwd1 at this point so no need to hide (again) """ On Mon, Mar 13, 2017 at 12:19:22PM -0400, Tom Lane wrote: > Date: Mon, 13 Mar 2017 12:19:22 -0400 > From: Tom Lane <*****@*****.**> > To: Adrian Klaver <*****@*****.**> > cc: Schmid Andreas <*****@*****.**>, > "'*****@*****.**'" <*****@*****.**> > Subject: Re: [GENERAL] createuser: How to specify a database to connect to > > Adrian Klaver <*****@*****.**> writes: > > On 03/13/2017 08:52 AM, Tom Lane wrote: > >> If by "history" you're worried about the server-side statement log, this > >> is merest fantasy: the createuser program is not magic, it just constructs > >> and sends a CREATE USER command for you. You'd actually be more secure > >> using psql, where (if you're superuser) you could shut off log_statement > >> for your session first. > > > There is a difference though: > > > psql> CREATE USER: > > > postgres-2017-03-13 09:03:27.147 PDT-0LOG: statement: create user > > dummy_user with login password '1234'; > > Well, what you're supposed to do is > > postgres=# create user dummy_user; > postgres=# \password dummy_user > Enter new password: > Enter it again: > postgres=# > > which will result in sending something like > > ALTER USER dummy_user PASSWORD 'md5c5e9567bc40082671d02c654260e0e09' > > You can additionally protect that by wrapping it into one transaction > (if you have a setup where the momentary existence of the role without a > password would be problematic) and/or shutting off logging beforehand. """ # this REALLY should be prefixed with md5 and the md5sum sent rather than the pwd cmd = """ALTER ROLE "%s" ENCRYPTED PASSWORD '%s';""" % ( dbo_account, dbo_pwd_new_2 ) gmPG2.run_rw_queries(link_obj = dbo_conn, queries = [{'cmd': cmd}], end_tx = True) return True
def encrypt_file_symmetric_7z(filename: str = None, passphrase: str = None, comment: str = None, verbose: bool = False, remove_unencrypted: bool = False) -> str: """Encrypt a file symmetrically with 7zip. Args: filename: the file to encrypt passphrase: minimum of 5 characters comment: a comment on the file to be put into a sidecar file, will also be encrypted remove_unencrypted: remove unencrypted source file if encryption succeeded Returns: Name of encrypted file or None. """ assert (filename is not None), '<filename> must not be None' assert (passphrase is not None), '<passphrase> must not be None' if len(passphrase) < 5: _log.error('<passphrase> must be at least 5 characters/signs/digits') return None gmLog2.add_word2hide(passphrase) #add 7z/winzip url to comment.txt _log.debug('attempting 7z AES encryption') for cmd in ['7z', '7z.exe']: found, binary = gmShellAPI.detect_external_binary(binary=cmd) if found: break if not found: _log.warning('no 7z binary found, trying gpg') return None if comment is not None: archive_path, archive_name = os.path.split(os.path.abspath(filename)) comment_filename = gmTools.get_unique_filename( prefix='%s.7z.comment-' % archive_name, tmp_dir=archive_path, suffix='.txt') with open(comment_filename, mode='wt', encoding='utf8', errors='replace') as comment_file: comment_file.write(comment) else: comment_filename = '' filename_encrypted = '%s.7z' % filename args = [ binary, 'a', '-bb3', '-mx0', "-p%s" % passphrase, filename_encrypted, filename, comment_filename ] encrypted, exit_code, stdout = gmShellAPI.run_process(cmd_line=args, encoding='utf8', verbose=verbose) gmTools.remove_file(comment_filename) if not encrypted: return None if not remove_unencrypted: return filename_encrypted if gmTools.remove_file(filename): return filename_encrypted gmTools.remove_file(filename_encrypted) return None
def _set_password(self, password=None): if password is not None: gmLog2.add_word2hide(password) self.__password = password _log.info('password was set')
def connect_to_database(max_attempts=3, expected_version=None, require_version=True): """Display the login dialog and try to log into the backend. - up to max_attempts times - returns True/False """ # force programmer to set a valid expected_version gmPG2.known_schema_hashes[expected_version] client_version = _cfg.get(option='client_version') global current_db_name current_db_name = 'gnumed_v%s' % expected_version attempt = 0 dlg = cLoginDialog(None, -1, client_version=client_version) dlg.Centre(wx.BOTH) while attempt < max_attempts: _log.debug('login attempt %s of %s', (attempt + 1), max_attempts) connected = False dlg.ShowModal() login = dlg.panel.GetLoginInfo() if login is None: _log.info("user cancelled login dialog") break # obscure unconditionally, it could be a valid password gmLog2.add_word2hide(login.password) # try getting a connection to verify the parameters do work creds = gmConnectionPool.cPGCredentials() creds.database = login.database creds.host = login.host creds.port = login.port creds.user = login.user creds.password = login.password pool = gmConnectionPool.gmConnectionPool() pool.credentials = creds try: conn = gmPG2.get_raw_connection(verbose=True, readonly=True) _log.info('successfully connected: %s', conn) connected = True except gmPG2.cAuthenticationError as exc: _log.exception('login attempt failed') gmPG2.log_pg_exception_details(exc) attempt += 1 if attempt < max_attempts: if ('host=127.0.0.1' in ('%s' % exc)) or ('host=' not in ('%s' % exc)): msg = msg_auth_error_local else: msg = msg_auth_error msg = msg % exc msg = regex.sub( r'password=[^\s]+', 'password=%s' % gmTools.u_replacement_character, msg) gmGuiHelpers.gm_show_error(msg, _('Connecting to backend')) del exc continue except gmPG2.dbapi.OperationalError as exc: _log.exception('login attempt failed') gmPG2.log_pg_exception_details(exc) msg = msg_login_problem_generic % exc msg = regex.sub(r'password=[^\s]+', 'password=%s' % gmTools.u_replacement_character, msg) gmGuiHelpers.gm_show_error(msg, _('Connecting to backend')) del exc continue conn.close() if not __database_is_acceptable_for_use( require_version=require_version, expected_version=expected_version, login=login): _log.info('database not suitable for use') connected = False break dlg.panel.save_state() gmExceptionHandlingWidgets.set_is_public_database( _cfg.get(option='is_public_db')) gmExceptionHandlingWidgets.set_helpdesk(_cfg.get(option='helpdesk')) _log.debug('establishing DB listener connection') conn = gmPG2.get_connection( verbose=True, connection_name='GNUmed-[DbListenerThread]', pooled=False) gmBackendListener.gmBackendListener(conn=conn) break dlg.DestroyLater() return connected