From 907f80e1889781a69edded00126b3e78ddeb7b8e Mon Sep 17 00:00:00 2001 From: Simeon Simeonov Date: Fri, 4 Mar 2022 20:48:20 +0100 Subject: Add the -P param, bash completion script and improve the README.md --- README.md | 33 +++++++--- completion/etoolkit.bash | 105 +++++++++++++++++++++++++++++++ etoolkit/__init__.py | 4 +- etoolkit/__main__.py | 79 ++++++++++++++++++++--- etoolkit/etoolkit.py | 6 +- tests/conftest.py | 31 +++++++-- tests/test_cli.py | 97 +++++++++++++++++++++++++++- tests/test_envtoolkit_instance.py | 8 ++- tests/test_envtoolkit_instance_static.py | 43 ++++++++----- 9 files changed, 363 insertions(+), 43 deletions(-) create mode 100644 completion/etoolkit.bash diff --git a/README.md b/README.md index 5c543f4..218ea7c 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ file are encrypted with the same master password. Setting a master password hash is a recommended but not mandatory. ```bash - etoolkit -p + etoolkit --generate-master-password-hash ``` That command will prompt for master password and output a hash that can then @@ -127,7 +127,7 @@ The hash is only used for verifying that a correct master password has been provided at a later time. Issuing: ```bash - etoolkit -e + etoolkit --encrypt-value ``` will prompt for the master password, then for the value to be encrypted and @@ -140,14 +140,24 @@ prompt if *-m* / *--multiple-values* parameter is provided. Manual decryption of single value(s): ```bash - etoolkit -d -m + etoolkit --decrypt-value --multiple-values ``` +Another possibility is to pass the value to *etoolkit*'s *stdin* using a pipe. +*etoolkit* will then only prompt for password and not for a value: + + ```bash + echo mysecret | etoolkit --encrypt-value + ``` + +... or if the *ETOOLKIT_MASTER_PASSWORD* env. variable is defined, its value +will be used instead of prompting for password. + Listing all available instances defined in the configuration file and then loading a specific instance can be achieved by: ```bash - etoolkit -l + etoolkit --list etoolkit ``` @@ -178,7 +188,7 @@ One can also spawn a different process than an interactive shell by using the *-s* / *--spawn* parameter. ```bash - etoolkit -s /bin/othershell + etoolkit --spawn /bin/othershell ``` Contact the author for questions and suggestions! :) @@ -264,10 +274,19 @@ file (f.i. ~/.bashrc): ```bash if [ -n "$ETOOLKIT_PROMPT" ]; then - export PS1="$ETOOLKIT_PROMPT$PS1" + export PS1="$ETOOLKIT_PROMPT$PS1" fi ``` +A quick and dirty bash completion for available instances can be set at the +bottom of your bash startup file: + + ```bash + complete -W '$(compgen -W "$(etoolkit -l)")' etoolkit + ``` + +A complete bash completion script for *etoolkit* can be found here: [https://github.com/blackm0re/etoolkit/blob/master/completion/etoolkit.bash](https://github.com/blackm0re/etoolkit/blob/master/completion/etoolkit.bash) + ## Support and contributing @@ -281,7 +300,7 @@ Simeon Simeonov - sgs @ LiberaChat ## [License](https://github.com/blackm0re/etoolkit/blob/master/LICENSE) -Copyright (c) 2021, Simeon Simeonov +Copyright (C) 2021-2022 Simeon Simeonov All rights reserved. [Licensed](https://github.com/blackm0re/etoolkit/blob/master/LICENSE) under the diff --git a/completion/etoolkit.bash b/completion/etoolkit.bash new file mode 100644 index 0000000..2e80811 --- /dev/null +++ b/completion/etoolkit.bash @@ -0,0 +1,105 @@ +# etoolkit bash-completion +# -*- shell-script -*- + +_instances() { + + local instances + + if [[ -n ${2} ]]; then + instances="$(etoolkit -c $2 -l 2> /dev/null)" + else + instances="$(etoolkit -l 2> /dev/null)" + fi + + if [[ $? -eq 0 ]]; then + COMPREPLY+=($(compgen -W "$instances" -- "$1")) + fi + +} + + +_etoolkit() { + + local all_params config_file c cur i no_output numwords prev spawn + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + numwords=${#COMP_WORDS[*]} + no_output=0 + spawn=0 + all_params="-d --decrypt-value -e --encrypt-value -l --list -h --help + -P --master-password-prompt -p --generate-master-password-hash + -c --config-file -E --echo -m --multiple-values -q --no-output + -s --spawn -v --version" + # if [ ${prev:0:1} == "-" ] + + if [ ${COMP_CWORD} -eq 1 ]; then + # first param + COMPREPLY=($(compgen -W "$all_params" -- "$cur")) + _instances "$cur" + return + else + # param >= 2 + # handle all instance-relevant params + for ((i = 0; i < ${numwords} + 1; i++ )); do + c=${COMP_WORDS[${i}]} + if [[ ${c} == "-c" || ${c} == "--config-file" ]]; then + config_file="${COMP_WORDS[${i} + 1]}" + elif [[ ${c} == "-s" || ${c} == "--spawn" ]]; then + spawn=1 + elif [[ ${c} == "-q" || ${c} == "--no-output" ]]; then + no_output=1 + fi + done + fi + + case $prev in + "-c" | "--config-file") + COMPREPLY=($(compgen -f -- "$cur")) + return + ;; + "-d" | "--decrypt-value") + COMPREPLY=($(compgen -W "-m --multiple-values -P --master-password-prompt" -- "$cur")) + return + ;; + "-e" | "--encrypt-value") + COMPREPLY=($(compgen -W "-E --echo -m --multiple-values -P --master-password-prompt" -- "$cur")) + return + ;; + "-E" | "--echo") + COMPREPLY=($(compgen -W "-e --encrypt-value -m --multiple-values -P --master-password-prompt" -- "$cur")) + return + ;; + "-m" | "--multiple-values") + COMPREPLY=($(compgen -W "-d --decrypt-value -E -e --echo --encrypt-value -P --master-password-prompt" -- "$cur")) + return + ;; + "-P" | "--master-password-prompt") + COMPREPLY=($(compgen -W "-d --decrypt-value -E -e --echo --encrypt-value -m --multiple-values" -- "$cur")) + return + ;; + "-s" | "--spawn") + COMPREPLY=($(compgen -c -- "$cur")) + return + ;; + esac + + # assume instance and handle only instance params + if [[ -z ${config_file} ]]; then + COMPREPLY+=($(compgen -W "-c --config-file" -- "$cur")) + fi + + if [[ ${spawn} -ne 1 ]]; then + COMPREPLY+=($(compgen -W "-s --spawn" -- "$cur")) + fi + + if [[ ${no_output} -ne 1 ]]; then + COMPREPLY+=($(compgen -W "--no-output -q" -- "$cur")) + fi + + _instances "$cur" "$config_file" + +} + + +complete -F _etoolkit etoolkit diff --git a/etoolkit/__init__.py b/etoolkit/__init__.py index 9a6d9aa..df53a55 100644 --- a/etoolkit/__init__.py +++ b/etoolkit/__init__.py @@ -1,5 +1,5 @@ # etoolkit -# Copyright (C) 2021 Simeon Simeonov +# Copyright (C) 2021-2022 Simeon Simeonov # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,7 +17,7 @@ from .etoolkit import EtoolkitInstance, EtoolkitInstanceError __author__ = 'Simeon Simeonov' -__version__ = '1.0.0' +__version__ = '1.1.0-rc1' __license__ = 'GPL3' diff --git a/etoolkit/__main__.py b/etoolkit/__main__.py index 8c4245d..603bb2e 100644 --- a/etoolkit/__main__.py +++ b/etoolkit/__main__.py @@ -1,5 +1,5 @@ # etoolkit -# Copyright (C) 2021 Simeon Simeonov +# Copyright (C) 2021-2022 Simeon Simeonov # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,6 +24,7 @@ python -m etoolkit -p import argparse import errno import getpass +import io import json import logging import os @@ -52,11 +53,29 @@ def decrypt_value(args: argparse.Namespace, config: dict): :type config: dict """ password_hash = None + pipe_input = None + if not os.isatty(sys.stdin.fileno()): + pipe_input = sys.stdin.read().strip() if 'general' in config: password_hash = config['general'].get('MASTER_PASSWORD_HASH') - password = etoolkit.EtoolkitInstance.confirm_password_prompt( - password_hash, False - ) + + if ( + args.master_password_prompt + or os.environ.get('ETOOLKIT_MASTER_PASSWORD') is None + ): + password = etoolkit.EtoolkitInstance.confirm_password_prompt( + password_hash, False + ) + else: + password = os.environ.get('ETOOLKIT_MASTER_PASSWORD') + + if pipe_input: + # the input came from stdin. No need to prompt + print( + 'Decrypted value: ' + f'{etoolkit.EtoolkitInstance.decrypt(password, pipe_input)}' + ) + return while True: try: value = input('Value: ') @@ -69,6 +88,7 @@ def decrypt_value(args: argparse.Namespace, config: dict): except KeyboardInterrupt: print(os.linesep) break + return def encrypt_value(args: argparse.Namespace, config: dict): @@ -86,9 +106,29 @@ def encrypt_value(args: argparse.Namespace, config: dict): :type config: dict """ password_hash = None + pipe_input = None + if not os.isatty(sys.stdin.fileno()): + pipe_input = sys.stdin.read().strip() if 'general' in config: password_hash = config['general'].get('MASTER_PASSWORD_HASH') - password = etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash) + + if ( + args.master_password_prompt + or os.environ.get('ETOOLKIT_MASTER_PASSWORD') is None + ): + password = etoolkit.EtoolkitInstance.confirm_password_prompt( + password_hash + ) + else: + password = os.environ.get('ETOOLKIT_MASTER_PASSWORD') + + if pipe_input: + # the input came from stdin. No need to prompt + print( + 'Encrypted value: ' + f'{etoolkit.EtoolkitInstance.encrypt(password, pipe_input)}' + ) + return while True: try: if args.echo: @@ -104,6 +144,7 @@ def encrypt_value(args: argparse.Namespace, config: dict): except KeyboardInterrupt: print(os.linesep) break + return def main(inargs=None): @@ -191,6 +232,16 @@ def main(inargs=None): '(Ctrl+C) (used together with -d / -e)' ), ) + parser.add_argument( + '-P', + '--master-password-prompt', + dest='master_password_prompt', + action='store_true', + help=( + 'Force prompt for the master password even if the env. variable ' + '"ETOOLKIT_MASTER_PASSWORD" is set' + ), + ) parser.add_argument( '-q', '--no-output', @@ -217,12 +268,26 @@ def main(inargs=None): ) args = parser.parse_args(inargs) try: - with open(args.config_file, 'r', encoding='utf-8') as fp: + with io.open(args.config_file, 'r', encoding='utf-8') as fp: config_dict = json.load(fp) + except FileNotFoundError as e: + # do not raise exception if config-file is missing for: + # - decrypting value + # - encrypting value + # - password hash generation + if args.password_hash or args.decrypt_value or args.encrypt_value: + logger.warning( + "Configuration file %s is missing, although not required " + "by the provided parameters", + args.config_file, + ) + config_dict = {} + else: + logger.error("Configuration file %s is missing", args.config_file) + raise SystemExit(errno.EIO) from e except Exception as e: logger.error("Unable to parse %r: %s", args.config_file, e) raise SystemExit(errno.EIO) from e - try: if args.decrypt_value: decrypt_value(args, config_dict) diff --git a/etoolkit/etoolkit.py b/etoolkit/etoolkit.py index 837d13a..d8221a5 100755 --- a/etoolkit/etoolkit.py +++ b/etoolkit/etoolkit.py @@ -1,5 +1,5 @@ # etoolkit -# Copyright (C) 2021 Simeon Simeonov +# Copyright (C) 2021-2022 Simeon Simeonov # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -179,7 +179,7 @@ class EtoolkitInstance: hashlib.scrypt( password.encode('utf-8'), salt=salt, - n=2 ** 14, + n=2**14, r=8, p=1, dklen=32, @@ -217,7 +217,7 @@ class EtoolkitInstance: hashlib.scrypt( password.encode('utf-8'), salt=salt, - n=2 ** 14, + n=2**14, r=8, p=1, dklen=32, diff --git a/tests/conftest.py b/tests/conftest.py index 1f70259..0770adb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ # etoolkit -# Copyright (C) 2021 Simeon Simeonov +# Copyright (C) 2021-2022 Simeon Simeonov # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,6 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Common fixtures""" +import json + import pytest @@ -23,8 +25,8 @@ def config_data(): return { 'general': { 'MASTER_PASSWORD_HASH': ( - 'pbkdf2_sha256$100000$kFOQkAPtStZ/Ny/O4501ygHGQnqh5Y+ySxF9qVHr' - 'iv8=$3BujuWzn3CfDnw4yiD9m3F+GjeW1MHHW40R/ThHNcn0=' + 'pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4m' + 'n80o=$h3PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo=' ) }, 'instances': { @@ -50,10 +52,27 @@ def config_data(): } +@pytest.fixture +def config_file(tmp_path, config_data): + """temporary config file for testing that includes config_data""" + cf = tmp_path / "etoolkit.json" + cf.write_text(json.dumps(config_data)) + return str(cf) + + +@pytest.fixture +def non_random_bytes_32(): + """always use the same bytes instead of os.urandom(32)""" + return ( + b'\xb9\x8aY3U_\x00j\xb4\x086K\xd9\xdb\x88N' + b'\xcd;\xe8$#\xfa\x12\x05\x12\x0c~\x17\x89\xa7\xf3J' + ) + + @pytest.fixture def password_hash(): - """password hash for testing, corresponding to 'the very secret passwd'""" + """password hash for testing, corresponding to 'The very secret passwd'""" return ( - 'pbkdf2_sha256$100000$kFOQkAPtStZ/Ny/O4501ygHGQnqh5Y+ySxF9qVHr' - 'iv8=$3BujuWzn3CfDnw4yiD9m3F+GjeW1MHHW40R/ThHNcn0=' + 'pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80o=$h3' + 'PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo=' ) diff --git a/tests/test_cli.py b/tests/test_cli.py index 1bd9be1..884e676 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,5 @@ # etoolkit -# Copyright (C) 2021 Simeon Simeonov +# Copyright (C) 2021-2022 Simeon Simeonov # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,12 +14,86 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Tests for the CLI (etoolkit.__main__""" +import errno +import os +import unittest.mock + import pytest import etoolkit from etoolkit.__main__ import main +@unittest.mock.patch('os.urandom') +@unittest.mock.patch('builtins.input') +def test_decrypt(binput, urandom, capsys, non_random_bytes_32, config_file): + """Tests encryption via the CLI interface""" + urandom.return_value = non_random_bytes_32 + binput.return_value = ( + 'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+' + 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw==' + ) + with unittest.mock.patch.dict( + os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'} + ): + with pytest.raises(SystemExit) as exit_info: + main(['-c', f'{config_file}', '-d']) + assert exit_info.type == SystemExit + assert exit_info.value.code == 0 + assert capsys.readouterr().out.strip() == 'Decrypted value: bar' + + +@unittest.mock.patch('os.urandom') +@unittest.mock.patch('builtins.input', lambda *args: 'bar') +def test_encrypt_with_echo(urandom, capsys, non_random_bytes_32, config_file): + """Tests encryption via the CLI interface""" + urandom.return_value = non_random_bytes_32 + with unittest.mock.patch.dict( + os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'} + ): + with pytest.raises(SystemExit) as exit_info: + main(['-c', f'{config_file}', '-e', '-E']) + assert exit_info.type == SystemExit + assert exit_info.value.code == 0 + assert capsys.readouterr().out.strip() == ( + 'Encrypted value: enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+' + 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw==' + ) + + +@unittest.mock.patch('os.urandom') +@unittest.mock.patch('getpass.getpass', lambda *args: 'bar') +def test_encrypt_without_echo(gpass, capsys, non_random_bytes_32, config_file): + """Tests encryption via the CLI interface""" + gpass.return_value = non_random_bytes_32 + with unittest.mock.patch.dict( + os.environ, {'ETOOLKIT_MASTER_PASSWORD': 'the very secret passwd'} + ): + with pytest.raises(SystemExit) as exit_info: + main(['-c', f'{config_file}', '-e']) + assert exit_info.type == SystemExit + assert exit_info.value.code == 0 + assert capsys.readouterr().out.strip() == ( + 'Encrypted value: enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+' + 'hIFEgx+F4mn80o=$xdF/1S+R2MGlEQMCOLG6OjEuzw==' + ) + + +def test_list(capsys, config_file): + """Tests list via the CLI interface""" + with pytest.raises(SystemExit) as exit_info: + main(['-l']) + assert exit_info.type == SystemExit + assert exit_info.value.code == errno.EIO + with pytest.raises(SystemExit) as exit_info: + main(['-c', f'{config_file}', '-l']) + assert exit_info.type == SystemExit + assert exit_info.value.code == 0 + assert capsys.readouterr().out.strip() == ( + f'default{os.linesep}dev{os.linesep}secret' + ) + + def test_help(capsys): """Dummy test checking if the CLI is available at all""" with pytest.raises(SystemExit) as exit_info: @@ -29,6 +103,27 @@ def test_help(capsys): assert capsys.readouterr().out.startswith('usage: etoolkit') +@unittest.mock.patch('os.urandom') +@unittest.mock.patch('getpass.getpass') +def test_generate_master_password_hash( + gpass, + urandom, + capsys, + non_random_bytes_32, +): + """Tests master password hash generation via the CLI interface""" + urandom.return_value = non_random_bytes_32 + gpass.return_value = 'The very secret passwd' + with pytest.raises(SystemExit) as exit_info: + main(['--generate-master-password-hash']) + assert exit_info.type == SystemExit + assert exit_info.value.code == 0 + assert capsys.readouterr().out.strip() == ( + 'Master password hash: pbkdf2_sha256$100000$uYpZM1VfAGq0CDZL2duITs076' + 'CQj+hIFEgx+F4mn80o=$h3PSPLCd37fP15zKdW4CBGn7CXE+q5UiydaF3vbeZHo=' + ) + + def test_version(capsys): """Dummy test checking if the CLI is available at all""" with pytest.raises(SystemExit) as exit_info: diff --git a/tests/test_envtoolkit_instance.py b/tests/test_envtoolkit_instance.py index 2ffacd2..9061dc8 100644 --- a/tests/test_envtoolkit_instance.py +++ b/tests/test_envtoolkit_instance.py @@ -1,5 +1,5 @@ # etoolkit -# Copyright (C) 2021 Simeon Simeonov +# Copyright (C) 2021-22 Simeon Simeonov # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,7 +19,7 @@ import pytest import etoolkit -def test_instantiation(config_data, password_hash): +def test_instantiation(config_data): """Tests for object instatiation""" with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: instance = etoolkit.EtoolkitInstance('devv', config_data) @@ -32,7 +32,9 @@ def test_instantiation(config_data, password_hash): assert 'DB_CONNECTION' not in instance.sensitive_env_variables assert 'PASSWORD' in instance.sensitive_env_variables assert instance.name == 'secret' - assert instance.master_password_hash == password_hash + assert instance.master_password_hash == ( + config_data['general']['MASTER_PASSWORD_HASH'] + ) assert instance.master_password is None diff --git a/tests/test_envtoolkit_instance_static.py b/tests/test_envtoolkit_instance_static.py index d7ac857..73a5f36 100644 --- a/tests/test_envtoolkit_instance_static.py +++ b/tests/test_envtoolkit_instance_static.py @@ -1,5 +1,5 @@ # etoolkit -# Copyright (C) 2021 Simeon Simeonov +# Copyright (C) 2021-2022 Simeon Simeonov # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,14 +24,14 @@ import etoolkit @unittest.mock.patch('getpass.getpass') def test_confirm_password_prompt(getpass, password_hash): """Tests the static EtoolkitInstance.confirm_password_prompt method""" - getpass.return_value = 'the very secret passwd' + getpass.return_value = 'The very secret passwd' assert ( etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash) - == 'the very secret passwd' + == 'The very secret passwd' ) assert ( etoolkit.EtoolkitInstance.confirm_password_prompt(password_hash, False) - == 'the very secret passwd' + == 'The very secret passwd' ) @@ -39,10 +39,10 @@ def test_decrypt(): """Tests the static EtoolkitInstance.decrypt method""" assert ( etoolkit.EtoolkitInstance.decrypt( - 'the very secret passwd', + 'The very secret passwd', ( - 'enc-val$1$Y/TBb1F3siHTw6qZg9ERzZfA8PLPf2CwGSQLpu9jYWw=$FT5tS9' - 'o+ABvsxogIXpJim16Gz5SVtV8=' + 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ4ZoAkurNgMYx' + 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY=' ), ) == 'secret1' @@ -51,11 +51,11 @@ def test_decrypt(): # now test with modified edata with pytest.raises(etoolkit.EtoolkitInstanceError) as exc_info: edata = ( - 'enc-val$1$Y/TBb1F3siHTw6qZg9ERzZfA8PLPf2CwGSQLpu9jYWw=$FT5tS9' - 'o+ABvsxogIXpJim17Gz5SVtV8=' + 'enc-val$1$/cXpEMoZrTlb9yokGhw8tLTSUkqnqJ5ZoAkurNgMYx' + 'w=$1VdkSMcZnLRwLiu1M8VlYcbelwmiVNY=' ) etoolkit.EtoolkitInstance.decrypt( - 'the very secret passwd', edata + 'The very secret passwd', edata ) == 'secret1' assert exc_info.type is etoolkit.EtoolkitInstanceError assert exc_info.value.args[0] == f'Invalid tag when decrypting: {edata}' @@ -70,17 +70,32 @@ def test_encrypt(): assert edata != etoolkit.EtoolkitInstance.encrypt('foo', 'bar') +@unittest.mock.patch('os.urandom') +def test_encrypt_staticly(urandom, non_random_bytes_32): + """Tests the EtoolkitInstance.encrypt method always with the same salt""" + urandom.return_value = non_random_bytes_32 + edata = etoolkit.EtoolkitInstance.encrypt('The very secret passwd', 'bar') + assert edata == ( + 'enc-val$1$uYpZM1VfAGq0CDZL2duITs076CQj+hIFEgx+F4mn80' + 'o=$HjPFNv6xC5hbMrFc0L5lSkWdfQ==' + ) + assert edata == etoolkit.EtoolkitInstance.encrypt( + 'The very secret passwd', + 'bar', + ) + + def test_get_new_password_hash(): """Tests the static EtoolkitInstance.get_new_password_hash method""" new_hash = etoolkit.EtoolkitInstance.get_new_password_hash( - 'the very secret passwd' + 'The very secret passwd' ) # all pbkdf2 params are the same / hardcoded for the time being assert new_hash.startswith('pbkdf2_sha256$100000$') assert len(new_hash) == 110 # the hash should always be different because of random salting assert new_hash != etoolkit.EtoolkitInstance.get_new_password_hash( - 'the very secret passwd' + 'The very secret passwd' ) @@ -95,8 +110,8 @@ def test_parse_value(): def test_password_matches(password_hash): """Tests the static EtoolkitInstance.password_matches method""" assert etoolkit.EtoolkitInstance.password_matches( - 'the very secret passwd', password_hash + 'The very secret passwd', password_hash ) assert not etoolkit.EtoolkitInstance.password_matches( - 'the very secret passwdo', password_hash + 'The very secret passwdo', password_hash ) -- cgit v1.3