コード例 #1
0
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
コード例 #2
0
ファイル: gmCrypto.py プロジェクト: weeksjm/gnumed
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
コード例 #3
0
	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
コード例 #4
0
ファイル: gmCrypto.py プロジェクト: rockdriven/gnumed
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
コード例 #5
0
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
コード例 #6
0
ファイル: gmAuthWidgets.py プロジェクト: ncqgm/gnumed
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
コード例 #7
0
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
コード例 #8
0
ファイル: gmCrypto.py プロジェクト: ncqgm/gnumed
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
コード例 #9
0
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
コード例 #10
0
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
コード例 #11
0
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
コード例 #12
0
ファイル: gmAuthWidgets.py プロジェクト: ncqgm/gnumed
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
コード例 #13
0
ファイル: gmCrypto.py プロジェクト: ncqgm/gnumed
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
コード例 #14
0
ファイル: gmAuthWidgets.py プロジェクト: ncqgm/gnumed
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
コード例 #15
0
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
コード例 #16
0
ファイル: gmConnectionPool.py プロジェクト: mpdo2017/gnumed
 def _set_password(self, password=None):
     if password is not None:
         gmLog2.add_word2hide(password)
     self.__password = password
     _log.info('password was set')
コード例 #17
0
ファイル: gmAuthWidgets.py プロジェクト: ncqgm/gnumed
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