diff --git a/securesystemslib/interface.py b/securesystemslib/interface.py index 9fddd60c..d0757cb6 100755 --- a/securesystemslib/interface.py +++ b/securesystemslib/interface.py @@ -237,11 +237,20 @@ def generate_and_write_rsa_keypair(filepath=None, bits=DEFAULT_RSA_KEY_BITS, def import_rsa_privatekey_from_file(filepath, password=None, - scheme='rsassa-pss-sha256'): + scheme='rsassa-pss-sha256', prompt=False): """ - Import the encrypted PEM file in 'filepath', decrypt it, and return the key - object in 'securesystemslib.formats.RSAKEY_SCHEMA' format. + Import the PEM file in 'filepath' containing the private key. + + If password is passed use passed password for decryption. + If prompt is True use entered password for decryption. + If no password is passed or entered, or if the entered password is an empty + string, omit decryption. + + Passing and prompting for a passowrd is not possible. + + The returned key is an object in the + 'securesystemslib.formats.RSAKEY_SCHEMA' format. filepath: @@ -254,18 +263,35 @@ def import_rsa_privatekey_from_file(filepath, password=None, scheme: The signature scheme used by the imported key. + prompt: + If True the user is prompted for a passphrase to decrypt 'filepath'. + Default is False. + + ValueError, if 'password' is passed and 'prompt' is True. + + ValueError, if 'password' is passed and it is an empty string. + securesystemslib.exceptions.FormatError, if the arguments are improperly formatted. - securesystemslib.exceptions.CryptoError, if 'filepath' is not a valid - encrypted key file. + securesystemslib.exceptions.FormatError, if the entered password is + improperly formatted. + + IOError, if 'filepath' can't be loaded. + + securesystemslib.exceptions.CryptoError, if a password is available + and 'filepath' is not a valid key file encrypted using that password. + + securesystemslib.exceptions.CryptoError, if no password is available + and 'filepath' is not a valid non-encrypted key file. - The contents of 'filepath' is read, decrypted, and the key stored. + The contents of 'filepath' are read, optionally decrypted, and returned. An RSA key object, conformant to 'securesystemslib.formats.RSAKEY_SCHEMA'. + """ # Does 'filepath' have the correct format? @@ -277,37 +303,51 @@ def import_rsa_privatekey_from_file(filepath, password=None, # Is 'scheme' properly formatted? securesystemslib.formats.RSA_SCHEME_SCHEMA.check_match(scheme) - # If the caller does not provide a password argument, prompt for one. - # Password confirmation disabled here, which should ideally happen only - # when creating encrypted key files (i.e., improve usability). - if password is None: # pragma: no cover + if password and prompt: + raise ValueError("Passing 'password' and 'prompt' True is not allowed.") + + # If 'password' was passed check format and that it is not empty. + if password is not None: + securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + # TODO: PASSWORD_SCHEMA should be securesystemslib.schema.AnyString(min=1) + if not len(password): + raise ValueError('Password must be 1 or more charcters') + + elif prompt: # pragma: no cover + # Password confirmation disabled here, which should ideally happen only + # when creating encrypted key files (i.e., improve usability). # It is safe to specify the full path of 'filepath' in the prompt and not # worry about leaking sensitive information about the key's location. # However, care should be taken when including the full path in exceptions # and log files. - password = get_password('Enter a password for the encrypted RSA' - ' file (' + Fore.RED + filepath + Fore.RESET + '): ', - confirm=False) + # NOTE: A user who gets prompted for a password, can only signal that the + # key is not encrypted by entering no password in the prompt, as opposed + # to a programmer who can call the function with or without a 'password'. + # Hence, we treat an empty password here, as if no 'password' was passed. + password = get_password('Enter a password for an encrypted RSA' + ' file \'' + Fore.RED + filepath + Fore.RESET + '\': ', + confirm=False) or None + + if password is not None: + # This check will not fail, because a mal-formatted passed password fails + # above and an entered password will always be a string (see get_password) + # However, we include it in case PASSWORD_SCHEMA or get_password changes. + securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) - # Does 'password' have the correct format? - securesystemslib.formats.PASSWORD_SCHEMA.check_match(password) + else: + logger.debug('No password was given. Attempting to import an' + ' unencrypted file.') - # Read the contents of 'filepath' that should be an encrypted PEM. + # Read the contents of 'filepath' that should be a PEM formatted private key. with open(filepath, 'rb') as file_object: - pem = file_object.read().decode('utf-8') + pem_key = file_object.read().decode('utf-8') # Convert 'pem_key' to 'securesystemslib.formats.RSAKEY_SCHEMA' format. # Raise 'securesystemslib.exceptions.CryptoError' if 'pem_key' is invalid. - if len(password): - rsa_key = securesystemslib.keys.import_rsakey_from_private_pem(pem, - scheme, password) - - else: - logger.debug('An empty password was given. Attempting to import an' - ' unencrypted file.') - rsa_key = securesystemslib.keys.import_rsakey_from_private_pem(pem, - scheme, password=None) + # If 'password' is None decryption will be omitted. + rsa_key = securesystemslib.keys.import_rsakey_from_private_pem(pem_key, + scheme, password) return rsa_key diff --git a/tests/test_interface.py b/tests/test_interface.py index d317e093..97b4eb05 100755 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -116,9 +116,8 @@ def test_generate_and_write_rsa_keypair(self): 'pw') self.assertTrue(securesystemslib.formats.RSAKEY_SCHEMA.matches(imported_privkey)) - # Try to import the unencrypted key file. - imported_privkey = interface.import_rsa_privatekey_from_file(test_keypath_unencrypted, - '') + # Try to import the unencrypted key file, by not passing a password + interface.import_rsa_privatekey_from_file(test_keypath_unencrypted) self.assertTrue(securesystemslib.formats.RSAKEY_SCHEMA.matches(imported_privkey)) # Custom 'bits' argument. @@ -167,7 +166,7 @@ def test_import_rsa_privatekey_from_file(self): # Load one of the pre-generated key files from # 'securesystemslib/tests/repository_data'. 'password' unlocks the # pre-generated key files. - key_filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), + key_filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'data', 'keystore', 'rsa_key') self.assertTrue(os.path.exists(key_filepath)) @@ -176,10 +175,22 @@ def test_import_rsa_privatekey_from_file(self): self.assertTrue(securesystemslib.formats.RSAKEY_SCHEMA.matches(imported_rsa_key)) - # Test improperly formatted argument. + # Test improperly formatted 'filepath' argument. self.assertRaises(securesystemslib.exceptions.FormatError, interface.import_rsa_privatekey_from_file, 3, 'pw') + # Test improperly formatted 'password' argument. + with self.assertRaises(securesystemslib.exceptions.FormatError): + interface.import_rsa_privatekey_from_file(key_filepath, 123) + + # Test unallowed empty 'password' + with self.assertRaises(ValueError): + interface.import_rsa_privatekey_from_file(key_filepath, '') + + # Test unallowed passing 'prompt' and 'password' + with self.assertRaises(ValueError): + interface.import_rsa_privatekey_from_file(key_filepath, + password='pw', prompt=True) # Test invalid argument. # Non-existent key file.