summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSimeon Simeonov2024-05-13 21:52:13 +0200
committerSimeon Simeonov2024-05-13 21:52:13 +0200
commit08a3280b062af83ee50fa139d7827d954907886e (patch)
tree8e1d3f3117d05b3ad779d070ec30ddbbe242e62b /src
parentbb9a844e22134a2537652ea14f93e82acb4ee380 (diff)
Implement re-encryption support2.0.0
Diffstat (limited to 'src')
-rw-r--r--src/etoolkit/__init__.py2
-rw-r--r--src/etoolkit/__main__.py371
-rw-r--r--src/etoolkit/etoolkit.py110
3 files changed, 336 insertions, 147 deletions
diff --git a/src/etoolkit/__init__.py b/src/etoolkit/__init__.py
index 0ef5957..711c6ae 100644
--- a/src/etoolkit/__init__.py
+++ b/src/etoolkit/__init__.py
@@ -18,7 +18,7 @@
18from .etoolkit import EtoolkitInstance, EtoolkitInstanceError 18from .etoolkit import EtoolkitInstance, EtoolkitInstanceError
19 19
20__author__ = 'Simeon Simeonov' 20__author__ = 'Simeon Simeonov'
21__version__ = '1.3.0' 21__version__ = '2.0.0'
22__license__ = 'GPL3' 22__license__ = 'GPL3'
23 23
24 24
diff --git a/src/etoolkit/__main__.py b/src/etoolkit/__main__.py
index 8b5bd55..a2f2f20 100644
--- a/src/etoolkit/__main__.py
+++ b/src/etoolkit/__main__.py
@@ -42,117 +42,244 @@ DEFAULT_LOG_LEVEL = logging.WARNING
42logger = logging.getLogger(__name__) 42logger = logging.getLogger(__name__)
43 43
44 44
45def decrypt_value(args: argparse.Namespace, config: dict): 45class EtoolkitCLIHandler:
46 """ 46 """
47 Interactive function for decrypting value(s) 47 Helper class used for handleing the growing amount of arguments
48 48
49 Prompts for master key password and then prompts for a value to decrypt 49 This class consists mostly of interactive methods and is not intended as
50 a part of the etoolkit API
51 """
50 52
51 The decrypted value is printed to stdout 53 def __init__(self, args: argparse.Namespace, config_dict: dict):
54 """
55 :param args: The parsed argparse arguments sent by the caller
56 :type args: argparse.Namespace
52 57
53 :param args: The arguments sent by the caller 58 :param config_dict: The config file structure
54 :type args: arparse.Namespace 59 :type config_dict: dict
60 """
61 self._args = args
62 self._config_dict = config_dict
55 63
56 :param config: The config dict sent by the caller 64 self._password_hash = None
57 :type config: dict 65 if 'general' in config_dict:
58 """ 66 self._password_hash = config_dict['general'].get(
59 password_hash = None 67 'MASTER_PASSWORD_HASH'
60 pipe_input = None 68 )
61 if not os.isatty(sys.stdin.fileno()):
62 pipe_input = sys.stdin.read().strip()
63 if 'general' in config:
64 password_hash = config['general'].get('MASTER_PASSWORD_HASH')
65 69
66 if ( 70 self._password_from_env = os.environ.get('ETOOLKIT_MASTER_PASSWORD')
67 args.master_password_prompt
68 or os.environ.get('ETOOLKIT_MASTER_PASSWORD') is None
69 ):
70 password = etoolkit.EtoolkitInstance.confirm_password_prompt(
71 password_hash, False
72 )
73 else:
74 password = os.environ.get('ETOOLKIT_MASTER_PASSWORD')
75 71
76 if pipe_input: 72 def decrypt_value(self):
77 # the input came from stdin. No need to prompt 73 """
78 print( 74 Interactive method for decrypting value(s)
79 'Decrypted value: ' 75
80 f'{etoolkit.EtoolkitInstance.decrypt(password, pipe_input)}' 76 Prompts for master key password and then prompts for a value to decrypt
81 ) 77
82 return 78 The decrypted value is printed to stdout
83 while True: 79 """
84 try: 80 pipe_input = None
85 value = input('Value: ') 81 if not os.isatty(sys.stdin.fileno()):
82 pipe_input = sys.stdin.read().strip()
83
84 if (
85 self._args.master_password_prompt
86 or self._password_from_env is None
87 ):
88 password = self._password_prompt()
89 else:
90 password = self._password_from_env
91
92 if pipe_input:
93 # the input came from stdin. No need to prompt
86 print( 94 print(
87 'Decrypted value: ' 95 'Decrypted value: '
88 f'{etoolkit.EtoolkitInstance.decrypt(password, value)}' 96 f'{etoolkit.EtoolkitInstance.decrypt(password, pipe_input)}'
89 ) 97 )
90 if not args.multiple_values: 98 return
99 while True:
100 try:
101 value = input('Value: ')
102 print(
103 'Decrypted value: '
104 f'{etoolkit.EtoolkitInstance.decrypt(password, value)}'
105 )
106 if not self._args.multiple_values:
107 break
108 except KeyboardInterrupt:
109 print(os.linesep)
91 break 110 break
92 except KeyboardInterrupt: 111 return
93 print(os.linesep)
94 break
95 return
96 112
113 def encrypt_value(self):
114 """
115 Interactive method for encrypting value(s)
97 116
98def encrypt_value(args: argparse.Namespace, config: dict): 117 Prompts for master key password and then prompts for a value to encrypt
99 """
100 Interactive function for encrypting value(s)
101 118
102 Prompts for master key password and then prompts for a value to encrypt 119 The encrypted value is printed to stdout
120 """
121 pipe_input = None
122 if not os.isatty(sys.stdin.fileno()):
123 pipe_input = sys.stdin.read().strip()
103 124
104 The encrypted value is printed to stdout 125 if (
126 self._args.master_password_prompt
127 or self._password_from_env is None
128 ):
129 password = self._password_prompt_confirm()
130 else:
131 password = self._password_from_env
105 132
106 :param args: The arguments sent by the caller 133 if pipe_input:
107 :type args: arparse.Namespace 134 # the input came from stdin. No need to prompt
135 print(
136 'Encrypted value: '
137 f'{etoolkit.EtoolkitInstance.encrypt(password, pipe_input)}'
138 )
139 return
108 140
109 :param config: The config dict sent by the caller 141 while True:
110 :type config: dict 142 try:
111 """ 143 if self._args.echo:
112 password_hash = None 144 value = input('Value: ')
113 pipe_input = None 145 else:
114 if not os.isatty(sys.stdin.fileno()): 146 value = getpass.getpass('Value: ')
115 pipe_input = sys.stdin.read().strip() 147 print(
116 if 'general' in config: 148 'Encrypted value: '
117 password_hash = config['general'].get('MASTER_PASSWORD_HASH') 149 f'{etoolkit.EtoolkitInstance.encrypt(password, value)}'
150 )
151 if not self._args.multiple_values:
152 break
153 except KeyboardInterrupt:
154 print(os.linesep)
155 break
156 return
157
158 def generate_master_password_hash(self):
159 """
160 Interactive method for generating password hash
161
162 Prompts for master key password and then for confirmation
118 163
119 if ( 164 The generated hash is printed to stdout
120 args.master_password_prompt 165 """
121 or os.environ.get('ETOOLKIT_MASTER_PASSWORD') is None 166 phash = etoolkit.EtoolkitInstance.get_new_password_hash(
122 ): 167 etoolkit.EtoolkitInstance.confirm_password_prompt()
123 password = etoolkit.EtoolkitInstance.confirm_password_prompt(
124 password_hash
125 ) 168 )
126 else: 169 print(f'Master password hash: {phash}')
127 password = os.environ.get('ETOOLKIT_MASTER_PASSWORD')
128 170
129 if pipe_input: 171 def list(self):
130 # the input came from stdin. No need to prompt 172 """Lists all instances defined in the config file"""
131 print( 173
132 'Encrypted value: ' 174 for instance_name in sorted(
133 f'{etoolkit.EtoolkitInstance.encrypt(password, pipe_input)}' 175 filter(
176 lambda s: not s.startswith('_'),
177 self._config_dict.get('instances', {}).keys(),
178 )
179 ):
180 print(instance_name)
181
182 def load_instance(self):
183 """Loads a single specified instance from the config file"""
184
185 inst = etoolkit.EtoolkitInstance(
186 self._args.instance, self._config_dict
134 ) 187 )
135 return 188
136 while True: 189 if (
137 try: 190 self._args.master_password_prompt
138 if args.echo: 191 or self._password_from_env is None
139 value = input('Value: ') 192 ):
140 else: 193 inst.prompt_func = (
141 value = getpass.getpass('Value: ') 194 etoolkit.EtoolkitInstance.confirm_password_prompt
195 )
196
197 env = inst.get_environ()
198
199 if self._args.dump_output:
200 print(inst.env_to_str(env))
201
202 os.environ.update(env)
203
204 if self._args.spawn:
205 subprocess.run(self._args.spawn.split(), check=False)
206 else:
207 subprocess.run(
208 os.environ.get('SHELL', 'bash').split(), check=False
209 )
210
211 def reencrypt(self):
212 """
213 Interactive method that prints new configuration data (JSON) to stdout
214
215 Prompts for master key password and then for a new password,
216 which may be the same as the current password
217
218 All existing encrypted values are decrypted using the current password
219 and then encrypted with the new password
220 """
221 print('(Current password) ', end='', flush=True)
222 if (
223 self._args.master_password_prompt
224 or self._password_from_env is None
225 ):
226 password = self._password_prompt()
227 else:
228 password = self._password_from_env
229
230 print('(New password) ', end='', flush=True)
231 new_password = etoolkit.EtoolkitInstance.confirm_password_prompt()
232
233 if self._args.reencrypt != 'all':
234 # re-encrypt a single instance
235 inst = etoolkit.EtoolkitInstance(
236 self._args.reencrypt, self._config_dict
237 )
142 print( 238 print(
143 'Encrypted value: ' 239 json.dumps(
144 f'{etoolkit.EtoolkitInstance.encrypt(password, value)}' 240 inst.get_reencrypted_instance_data(new_password, password),
241 indent=4,
242 )
145 ) 243 )
146 if not args.multiple_values: 244 return
147 break 245
148 except KeyboardInterrupt: 246 # re-encrypt all
149 print(os.linesep) 247 new_config_dict = dict(self._config_dict)
150 break 248 if (
151 return 249 'general' in new_config_dict
250 and 'MASTER_PASSWORD_HASH' in new_config_dict['general']
251 ):
252 new_config_dict['general']['MASTER_PASSWORD_HASH'] = (
253 etoolkit.EtoolkitInstance.get_new_password_hash(new_password)
254 )
255
256 for instance_name in self._config_dict['instances']:
257 inst = etoolkit.EtoolkitInstance(instance_name, self._config_dict)
258 new_config_dict['instances'][instance_name] = (
259 inst.get_reencrypted_instance_data(new_password, password)
260 )
261 print(json.dumps(new_config_dict, indent=4))
262
263 def _password_prompt(self) -> str:
264 """
265 Wrapper for EtoolkitInstance.confirm_password_prompt(confirm=False)
266 """
267 return etoolkit.EtoolkitInstance.confirm_password_prompt(
268 self._password_hash, False
269 )
270
271 def _password_prompt_confirm(self) -> str:
272 """
273 Wrapper for EtoolkitInstance.confirm_password_prompt(confirm=True)
274 """
275 return etoolkit.EtoolkitInstance.confirm_password_prompt(
276 self._password_hash
277 )
152 278
153 279
154def main(inargs=None): 280def main(inargs=None):
155 """main entry point""" 281 """main entry point"""
282
156 parser = argparse.ArgumentParser( 283 parser = argparse.ArgumentParser(
157 prog=__package__, 284 prog=__package__,
158 epilog=( 285 epilog=(
@@ -207,6 +334,20 @@ def main(inargs=None):
207 required=False, 334 required=False,
208 help='Prompt for master password, display the generated hash and exit', 335 help='Prompt for master password, display the generated hash and exit',
209 ) 336 )
337 group.add_argument(
338 '-r',
339 '--reencrypt',
340 metavar='<instance | all>',
341 type=str,
342 default='',
343 dest='reencrypt',
344 required=False,
345 help=(
346 'Prompt for current master password, new master password and '
347 're-encrypt either all encrypted values or only those for a '
348 'given instance'
349 ),
350 )
210 parser.add_argument( 351 parser.add_argument(
211 '-c', 352 '-c',
212 '--config-file', 353 '--config-file',
@@ -274,7 +415,7 @@ def main(inargs=None):
274 try: 415 try:
275 with io.open(args.config_file, encoding='utf-8') as fp: 416 with io.open(args.config_file, encoding='utf-8') as fp:
276 config_dict = json.load(fp) 417 config_dict = json.load(fp)
277 except FileNotFoundError as e: 418 except FileNotFoundError as err:
278 # do not raise exception if config-file is missing for: 419 # do not raise exception if config-file is missing for:
279 # - decrypting value 420 # - decrypting value
280 # - encrypting value 421 # - encrypting value
@@ -288,66 +429,38 @@ def main(inargs=None):
288 config_dict = {} 429 config_dict = {}
289 else: 430 else:
290 logger.error('Configuration file %s is missing', args.config_file) 431 logger.error('Configuration file %s is missing', args.config_file)
291 raise SystemExit(errno.EIO) from e 432 raise SystemExit(errno.EIO) from err
292 except Exception as e: 433 except Exception as exp:
293 logger.exception('Unable to parse %r', args.config_file) 434 logger.exception('Unable to parse %r', args.config_file)
294 raise SystemExit(errno.EIO) from e 435 raise SystemExit(errno.EIO) from exp
295 try: 436 try:
437 etoolkit_cli_handler = EtoolkitCLIHandler(args, config_dict)
296 if args.decrypt_value: 438 if args.decrypt_value:
297 decrypt_value(args, config_dict) 439 etoolkit_cli_handler.decrypt_value()
298 sys.exit(0) 440 sys.exit(0)
299 if args.encrypt_value: 441 if args.encrypt_value:
300 encrypt_value(args, config_dict) 442 etoolkit_cli_handler.encrypt_value()
301 sys.exit(0) 443 sys.exit(0)
302 if args.password_hash: 444 if args.password_hash:
303 master_password = ( 445 etoolkit_cli_handler.generate_master_password_hash()
304 etoolkit.EtoolkitInstance.confirm_password_prompt()
305 )
306 phash = etoolkit.EtoolkitInstance.get_new_password_hash(
307 master_password
308 )
309 print(f'Master password hash: {phash}')
310 sys.exit(0) 446 sys.exit(0)
311 if args.list: 447 if args.list:
312 for instance_name in sorted( 448 etoolkit_cli_handler.list()
313 filter( 449 sys.exit(0)
314 lambda s: not s.startswith('_'), 450 if args.reencrypt:
315 config_dict.get('instances', {}).keys(), 451 etoolkit_cli_handler.reencrypt()
316 )
317 ):
318 print(instance_name)
319 sys.exit(0) 452 sys.exit(0)
320 453
321 inst = etoolkit.EtoolkitInstance(args.instance, config_dict) 454 etoolkit_cli_handler.load_instance()
322 if (
323 args.master_password_prompt
324 or os.environ.get('ETOOLKIT_MASTER_PASSWORD') is None
325 ):
326 inst.prompt_func = (
327 etoolkit.EtoolkitInstance.confirm_password_prompt
328 )
329 env = inst.get_environ()
330
331 if args.dump_output:
332 inst.dump_env(env)
333
334 os.environ.update(env)
335
336 if args.spawn:
337 subprocess.run(args.spawn.split(), check=False)
338 else:
339 subprocess.run(
340 os.environ.get('SHELL', 'bash').split(), check=False
341 )
342 except KeyboardInterrupt: 455 except KeyboardInterrupt:
343 logger.debug('KeyboardInterrupt') 456 logger.debug('KeyboardInterrupt')
344 print(os.linesep) 457 print(os.linesep)
345 sys.exit(0) 458 sys.exit(0)
346 except etoolkit.EtoolkitInstanceError as e: 459 except etoolkit.EtoolkitInstanceError as err:
347 logger.error('EtoolkitInstanceError: %s', e) 460 logger.error('EtoolkitInstanceError: %s', err)
348 sys.exit(1) 461 sys.exit(1)
349 except subprocess.CalledProcessError as e: 462 except subprocess.CalledProcessError as err:
350 logger.error('Unable to spawn shell process: %s', e) 463 logger.error('Unable to spawn shell process: %s', err)
351 sys.exit(1) 464 sys.exit(1)
352 except Exception: 465 except Exception:
353 logger.exception('Unexpected exception') 466 logger.exception('Unexpected exception')
diff --git a/src/etoolkit/etoolkit.py b/src/etoolkit/etoolkit.py
index a2e0d7a..9a1b47e 100644
--- a/src/etoolkit/etoolkit.py
+++ b/src/etoolkit/etoolkit.py
@@ -23,7 +23,6 @@ import os
23from cryptography.exceptions import InvalidTag 23from cryptography.exceptions import InvalidTag
24from cryptography.hazmat.primitives.ciphers.aead import AESGCM 24from cryptography.hazmat.primitives.ciphers.aead import AESGCM
25 25
26
27MIN_ENCRYPTED_VALUE_LENGTH = 32 26MIN_ENCRYPTED_VALUE_LENGTH = 32
28 27
29 28
@@ -50,24 +49,26 @@ class EtoolkitInstance:
50 self._master_password_hash = None 49 self._master_password_hash = None
51 self._prompt_func = None # function to use when prompting for input 50 self._prompt_func = None # function to use when prompting for input
52 try: 51 try:
53 inst_data = data['instances'][name] 52 self._instance_data = data['instances'][name]
54 except KeyError as e: 53 except KeyError as err:
55 raise EtoolkitInstanceError(f'Unknown instance "{name}"') from e 54 raise EtoolkitInstanceError(f'Unknown instance "{name}"') from err
56 if inst_data.get('ETOOLKIT_PARENT'): 55 if self._instance_data.get('ETOOLKIT_PARENT'):
57 self._parent = EtoolkitInstance(inst_data['ETOOLKIT_PARENT'], data) 56 self._parent = EtoolkitInstance(
57 self._instance_data['ETOOLKIT_PARENT'], data
58 )
58 self._raw_env_variables.update(self._parent.raw_env_variables) 59 self._raw_env_variables.update(self._parent.raw_env_variables)
59 self._sensitive_env_variables.extend( 60 self._sensitive_env_variables.extend(
60 self._parent.sensitive_env_variables 61 self._parent.sensitive_env_variables
61 ) 62 )
62 if inst_data.get('ETOOLKIT_SENSITIVE'): 63 if self._instance_data.get('ETOOLKIT_SENSITIVE'):
63 if not isinstance(inst_data['ETOOLKIT_SENSITIVE'], list): 64 if not isinstance(self._instance_data['ETOOLKIT_SENSITIVE'], list):
64 raise EtoolkitInstanceError( 65 raise EtoolkitInstanceError(
65 '"ETOOLKIT_SENSITIVE" must be a list' 66 '"ETOOLKIT_SENSITIVE" must be a list'
66 ) 67 )
67 self._sensitive_env_variables.extend( 68 self._sensitive_env_variables.extend(
68 inst_data['ETOOLKIT_SENSITIVE'] 69 self._instance_data['ETOOLKIT_SENSITIVE']
69 ) 70 )
70 self._raw_env_variables.update(inst_data) 71 self._raw_env_variables.update(self._instance_data)
71 # remove non env. variable data 72 # remove non env. variable data
72 self._raw_env_variables.pop('ETOOLKIT_PARENT', None) 73 self._raw_env_variables.pop('ETOOLKIT_PARENT', None)
73 self._raw_env_variables.pop('ETOOLKIT_SENSITIVE', None) 74 self._raw_env_variables.pop('ETOOLKIT_SENSITIVE', None)
@@ -200,7 +201,7 @@ class EtoolkitInstance:
200 # padding_length_bytes(2 bytes) data padding (between 0 and 32) 201 # padding_length_bytes(2 bytes) data padding (between 0 and 32)
201 202
202 # extract padding_length_bytes 203 # extract padding_length_bytes
203 if data[:2] == b'--': 204 if data[:2] == b'-1' or data[:2] == b'--':
204 data = data[2:] 205 data = data[2:]
205 else: 206 else:
206 data = data[2 : -int(data[:2].decode())] 207 data = data[2 : -int(data[:2].decode())]
@@ -259,7 +260,7 @@ class EtoolkitInstance:
259 ) 260 )
260 ) 261 )
261 nonce = salt[:12] 262 nonce = salt[:12]
262 padding_length_bytes = b'--' # no padding used 2 bytes "sign" 263 padding_length_bytes = b'-1' # no padding used 2 bytes "sign"
263 edata = aesgcm.encrypt( 264 edata = aesgcm.encrypt(
264 nonce, padding_length_bytes + data_bytes, salt 265 nonce, padding_length_bytes + data_bytes, salt
265 ) 266 )
@@ -347,18 +348,49 @@ class EtoolkitInstance:
347 except Exception: 348 except Exception:
348 return False 349 return False
349 350
350 def dump_env(self, env: dict): 351 @staticmethod
352 def reencrypt(password: str, new_password: str, edata: str) -> str:
353 """
354 Re-encrypts `edata` using `password` and `new_password`.
355
356 Version 2 of the etoolkit encryption format
357
358 `edata` is in the following format:
359 enc-val$`version-num`$`bas64-salt`$`base64-encrypted_data`
360
361 :param password: The password to decrypt `edata` with
362 :type password: str
363
364 :param new_password: The password to re-encrypt the plain-text with
365 :type new_password: str
366
367 :param edata: The data to be re-encrypted
368 :type edata: str
369
370 :return: The new encrypted string string
371 :rtype: str
372 """
373 return EtoolkitInstance.encrypt(
374 new_password, EtoolkitInstance.decrypt(password, edata)
375 )
376
377 def env_to_str(self, env: dict) -> str:
351 """ 378 """
352 Prints an environment dict to stdout. 379 Returns a printable str. representation of the environment dict
353 380
354 :param env: The environment dict 381 :param env: The environment dict
355 :type env: dict 382 :type env: dict
383
384 :return: Printable representation of the environment dict
385 :rtype: str
356 """ 386 """
387 env_str = ''
357 for key, value in env.items(): 388 for key, value in env.items():
358 if key in self._sensitive_env_variables: 389 if key in self._sensitive_env_variables:
359 print(f'{key}: ***') 390 env_str += f'{key}: ***{os.linesep}'
360 continue 391 continue
361 print(f'{key}: {value}') 392 env_str += f'{key}: {value}{os.linesep}'
393 return env_str
362 394
363 def get_environ(self) -> dict: 395 def get_environ(self) -> dict:
364 """ 396 """
@@ -421,6 +453,50 @@ class EtoolkitInstance:
421 return self.name 453 return self.name
422 return self._parent.get_full_name(delimiter) + delimiter + self.name 454 return self._parent.get_full_name(delimiter) + delimiter + self.name
423 455
456 def get_reencrypted_instance_data(
457 self, new_password: str, password: str = None
458 ) -> dict:
459 """
460 Returns new instance data (dict) containing new encrypted values
461
462 Each encrypted value in this instance is decrypted using `password`
463 and then encrypted again using `new_password`
464
465 If `password` is None, master_password is not set earlier for this
466 instance and 'ETOOLKIT_MASTER_PASSWORD' is not set,
467 the prompt function will be called
468
469 :param new_password: The password to reencrypt with
470 :type new_password: str
471
472 :param password: The password to decrypt current encrypted values with
473 :type password: str or None
474
475 :return: New instance data
476 :rtype: dict
477 """
478 if password is None:
479 password = self._master_password
480
481 if password is None and self._prompt_func is None:
482 password = os.environ.get('ETOOLKIT_MASTER_PASSWORD')
483 if password is None:
484 raise EtoolkitInstanceError(
485 'Neither password or prompt function set'
486 )
487
488 if password is None:
489 password = self._prompt_func(
490 self._master_password_hash, confirm=False
491 )
492
493 new_data = dict(self._instance_data)
494 for key, value in self._instance_data.items():
495 if isinstance(value, str) and value.startswith('enc-val$'):
496 new_data[key] = self.reencrypt(password, new_password, value)
497
498 return new_data
499
424 def _decrypt_value(self, evalue: str) -> str: 500 def _decrypt_value(self, evalue: str) -> str:
425 """ 501 """
426 Decrypts an encrypted value using the master password 502 Decrypts an encrypted value using the master password
@@ -452,4 +528,4 @@ class EtoolkitInstance:
452 self.master_password = self._prompt_func( 528 self.master_password = self._prompt_func(
453 self._master_password_hash, confirm=False 529 self._master_password_hash, confirm=False
454 ) 530 )
455 return EtoolkitInstance.decrypt(self._master_password, evalue) 531 return self.decrypt(self._master_password, evalue)