#!/usr/bin/env python

"""
<Program Name>
  test_interface.py

<Author>
  Vladimir Diaz <vladimir.v.diaz@gmail.com>

<Started>
  January 5, 2017.

<Copyright>
  See LICENSE for licensing information.

<Purpose>
  Unit test for 'interface.py'.
"""

import os
import shutil
import stat
import tempfile
import unittest
from unittest import mock

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key

from securesystemslib import (  # pylint: disable=wrong-import-position
    KEY_TYPE_ECDSA,
    KEY_TYPE_ED25519,
    KEY_TYPE_RSA,
)
from securesystemslib.exceptions import (  # pylint: disable=wrong-import-position
    CryptoError,
    Error,
    FormatError,
)
from securesystemslib.formats import (  # pylint: disable=wrong-import-position
    ANY_PUBKEY_DICT_SCHEMA,
    ECDSAKEY_SCHEMA,
    ED25519KEY_SCHEMA,
    PUBLIC_KEY_SCHEMA,
    RSAKEY_SCHEMA,
)
from securesystemslib.interface import (  # pylint: disable=wrong-import-position
    _generate_and_write_ecdsa_keypair,
    _generate_and_write_ed25519_keypair,
    _generate_and_write_rsa_keypair,
    generate_and_write_ecdsa_keypair,
    generate_and_write_ecdsa_keypair_with_prompt,
    generate_and_write_ed25519_keypair,
    generate_and_write_ed25519_keypair_with_prompt,
    generate_and_write_rsa_keypair,
    generate_and_write_rsa_keypair_with_prompt,
    generate_and_write_unencrypted_ecdsa_keypair,
    generate_and_write_unencrypted_ed25519_keypair,
    generate_and_write_unencrypted_rsa_keypair,
    import_ecdsa_privatekey_from_file,
    import_ecdsa_publickey_from_file,
    import_ed25519_privatekey_from_file,
    import_ed25519_publickey_from_file,
    import_privatekey_from_file,
    import_publickeys_from_file,
    import_rsa_privatekey_from_file,
    import_rsa_publickey_from_file,
)


class TestInterfaceFunctions(
    unittest.TestCase
):  # pylint: disable=missing-class-docstring
    @classmethod
    def setUpClass(cls):
        cls.test_data_dir = os.path.join(
            os.path.dirname(os.path.realpath(__file__)), "data"
        )

        cls.path_rsa = os.path.join(cls.test_data_dir, "keystore", "rsa_key")
        cls.path_ed25519 = os.path.join(
            cls.test_data_dir, "keystore", "ed25519_key"
        )
        cls.path_ecdsa = os.path.join(
            cls.test_data_dir, "keystore", "ecdsa_key"
        )
        cls.path_no_key = os.path.join(cls.test_data_dir, "keystore", "no_key")

        cls.orig_cwd = os.getcwd()

    def setUp(self):
        self.tmp_dir = tempfile.mkdtemp(dir=self.orig_cwd)
        os.chdir(self.tmp_dir)

    def tearDown(self):
        os.chdir(self.orig_cwd)
        shutil.rmtree(self.tmp_dir)

    def test_rsa(self):  # pylint: disable=too-many-locals,too-many-statements
        """Test RSA key _generation and import interface functions."""

        # TEST: Generate default keys and import
        # Assert location and format
        fn_default = "default"
        fn_default_ret = _generate_and_write_rsa_keypair(filepath=fn_default)

        pub = import_rsa_publickey_from_file(fn_default + ".pub")
        priv = import_rsa_privatekey_from_file(fn_default)

        self.assertEqual(fn_default, fn_default_ret)
        self.assertTrue(RSAKEY_SCHEMA.matches(pub))
        self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub))
        self.assertTrue(RSAKEY_SCHEMA.matches(priv))
        # NOTE: There is no private key schema, at least check it has a value
        self.assertTrue(priv["keyval"]["private"])

        # TEST: Generate unencrypted keys with empty prompt
        # Assert importable without password
        fn_empty_prompt = "empty_prompt"
        with mock.patch(
            "securesystemslib.interface.get_password", return_value=""
        ):
            _generate_and_write_rsa_keypair(
                filepath=fn_empty_prompt, prompt=True
            )
        import_rsa_privatekey_from_file(fn_empty_prompt)

        # TEST: Generate keys with auto-filename, i.e. keyid
        # Assert filename is keyid
        fn_keyid = _generate_and_write_rsa_keypair()
        pub = import_rsa_publickey_from_file(fn_keyid + ".pub")
        priv = import_rsa_privatekey_from_file(fn_keyid)
        self.assertTrue(
            os.path.basename(fn_keyid) == pub["keyid"] == priv["keyid"]
        )

        # TEST: Generate keys with custom bits
        # Assert length
        bits = 4096
        fn_bits = "bits"
        _generate_and_write_rsa_keypair(filepath=fn_bits, bits=bits)

        priv = import_rsa_privatekey_from_file(fn_bits)
        # NOTE: Parse PEM with pyca/cryptography to get the key size property
        obj_bits = load_pem_private_key(
            priv["keyval"]["private"].encode("utf-8"),
            password=None,
            backend=default_backend(),
        )

        self.assertEqual(obj_bits.key_size, bits)

        # TEST: Generate two keypairs with encrypted private keys using ...
        pw = "pw"
        fn_encrypted = "encrypted"
        fn_prompt = "prompt"

        # ... a passed pw ...
        _generate_and_write_rsa_keypair(filepath=fn_encrypted, password=pw)
        with mock.patch(
            "securesystemslib.interface.get_password", return_value=pw
        ):
            # ... and a prompted pw.
            _generate_and_write_rsa_keypair(filepath=fn_prompt, prompt=True)

            # Assert that both private keys are importable using the prompted pw ...
            import_rsa_privatekey_from_file(fn_prompt, prompt=True)
            import_rsa_privatekey_from_file(fn_encrypted, prompt=True)

        # ... and the passed pw.
        import_rsa_privatekey_from_file(fn_prompt, password=pw)
        import_rsa_privatekey_from_file(fn_encrypted, password=pw)

        # TEST: Import existing keys with encrypted private key (test regression)
        # Assert format
        pub = import_rsa_publickey_from_file(self.path_rsa + ".pub")
        priv = import_rsa_privatekey_from_file(self.path_rsa, "password")

        self.assertTrue(RSAKEY_SCHEMA.matches(pub))
        self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub))
        self.assertTrue(RSAKEY_SCHEMA.matches(priv))
        # NOTE: There is no private key schema, at least check it has a value
        self.assertTrue(priv["keyval"]["private"])

        # TEST: Generation errors
        for idx, (kwargs, err_msg) in enumerate(
            [
                # Error on empty password
                (
                    {"password": ""},
                    "encryption password must be 1 or more characters long",
                ),
                # Error on 'password' and 'prompt=True'
                (
                    {"password": pw, "prompt": True},
                    "passing 'password' and 'prompt=True' is not allowed",
                ),
            ]
        ):

            with self.assertRaises(
                ValueError,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ) as ctx:
                _generate_and_write_rsa_keypair(**kwargs)

            self.assertEqual(
                err_msg,
                str(ctx.exception),
                "expected: '{}' got: '{}' (row {})".format(  # pylint: disable=consider-using-f-string
                    err_msg, ctx.exception, idx
                ),
            )

        # Error on bad argument format
        for idx, kwargs in enumerate(
            [
                {"bits": 1024},  # Too low
                {"bits": "not-an-int"},
                {"filepath": 123456},  # Not a string
                {"password": 123456},  # Not a string
                {"prompt": "not-a-bool"},
            ]
        ):
            with self.assertRaises(
                FormatError,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ):
                _generate_and_write_rsa_keypair(**kwargs)

        # TEST: Import errors

        # Error public key import
        err_msg = "Invalid public pem"
        with self.assertRaises(Error) as ctx:
            import_rsa_publickey_from_file(fn_default)

        self.assertTrue(
            err_msg in str(ctx.exception),
            "expected: '{}' got: '{}'".format(  # pylint: disable=consider-using-f-string
                err_msg, ctx.exception
            ),
        )

        # Error on private key import...
        for idx, (args, kwargs, err, err_msg) in enumerate(
            [
                # Error on not a private key
                (
                    [fn_default + ".pub"],
                    {},
                    CryptoError,
                    "Could not deserialize key data",
                ),
                # Error on not encrypted
                (
                    [fn_default],
                    {"password": pw},
                    CryptoError,
                    "Password was given but private key is not encrypted",
                ),
                # Error on encrypted but no pw
                (
                    [fn_encrypted],
                    {},
                    CryptoError,
                    "Password was not given but private key is encrypted",
                ),
                # Error on encrypted but empty pw passed
                (
                    [fn_encrypted],
                    {"password": ""},
                    CryptoError,
                    "Password was not given but private key is encrypted",
                ),
                # Error on encrypted but bad pw passed
                (
                    [fn_encrypted],
                    {"password": "bad pw"},
                    CryptoError,
                    "Bad decrypt. Incorrect password?",
                ),
                # Error on pw and prompt
                (
                    [fn_default],
                    {"password": pw, "prompt": True},
                    ValueError,
                    "passing 'password' and 'prompt=True' is not allowed",
                ),
            ]
        ):

            with self.assertRaises(
                err,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ) as ctx:
                import_rsa_privatekey_from_file(*args, **kwargs)

            self.assertTrue(
                err_msg in str(ctx.exception),
                "expected: '{}' got: '{}' (row {})".format(  # pylint: disable=consider-using-f-string
                    err_msg, ctx.exception, idx
                ),
            )

        # Error on encrypted but bad pw prompted
        err_msg = "Password was not given but private key is encrypted"
        with self.assertRaises(CryptoError) as ctx, mock.patch(
            "securesystemslib.interface.get_password", return_value="bad_pw"
        ):
            import_rsa_privatekey_from_file(fn_encrypted)

        self.assertTrue(
            err_msg in str(ctx.exception),
            "expected: '{}' got: '{}'".format(  # pylint: disable=consider-using-f-string
                err_msg, ctx.exception
            ),
        )

        # Error on bad argument format
        for idx, (args, kwargs) in enumerate(
            [
                ([123456], {}),  # bad path
                ([fn_default], {"scheme": 123456}),  # bad scheme
                ([fn_default], {"scheme": "bad scheme"}),  # bad scheme
            ]
        ):
            with self.assertRaises(
                FormatError,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ):
                import_rsa_publickey_from_file(*args, **kwargs)
            with self.assertRaises(
                FormatError,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ):
                import_rsa_privatekey_from_file(*args, **kwargs)

        # bad password
        with self.assertRaises(FormatError):
            import_rsa_privatekey_from_file(fn_default, password=123456)

        # bad prompt
        with self.assertRaises(FormatError):
            import_rsa_privatekey_from_file(fn_default, prompt="not-a-bool")

    def test_ed25519(
        self,
    ):  # pylint: disable=too-many-locals,too-many-statements
        """Test ed25519 key _generation and import interface functions."""

        # TEST: Generate default keys and import
        # Assert location and format
        fn_default = "default"
        fn_default_ret = _generate_and_write_ed25519_keypair(
            filepath=fn_default
        )

        pub = import_ed25519_publickey_from_file(fn_default + ".pub")
        priv = import_ed25519_privatekey_from_file(fn_default)

        self.assertEqual(fn_default, fn_default_ret)
        self.assertTrue(ED25519KEY_SCHEMA.matches(pub))
        self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub))
        self.assertTrue(ED25519KEY_SCHEMA.matches(priv))
        # NOTE: There is no private key schema, at least check it has a value
        self.assertTrue(priv["keyval"]["private"])

        # TEST: Generate unencrypted keys with empty prompt
        # Assert importable with empty prompt password and without password
        fn_empty_prompt = "empty_prompt"
        with mock.patch(
            "securesystemslib.interface.get_password", return_value=""
        ):
            _generate_and_write_ed25519_keypair(filepath=fn_empty_prompt)
            import_ed25519_privatekey_from_file(fn_empty_prompt, prompt=True)
        import_ed25519_privatekey_from_file(fn_empty_prompt)

        # TEST: Generate keys with auto-filename, i.e. keyid
        # Assert filename is keyid
        fn_keyid = _generate_and_write_ed25519_keypair()
        pub = import_ed25519_publickey_from_file(fn_keyid + ".pub")
        priv = import_ed25519_privatekey_from_file(fn_keyid)
        self.assertTrue(
            os.path.basename(fn_keyid) == pub["keyid"] == priv["keyid"]
        )

        # TEST: Generate two keypairs with encrypted private keys using ...
        pw = "pw"
        fn_encrypted = "encrypted"
        fn_prompt = "prompt"
        # ... a passed pw ...
        _generate_and_write_ed25519_keypair(filepath=fn_encrypted, password=pw)
        with mock.patch(
            "securesystemslib.interface.get_password", return_value=pw
        ):
            # ... and a prompted pw.
            _generate_and_write_ed25519_keypair(filepath=fn_prompt, prompt=True)

            # Assert that both private keys are importable using the prompted pw ...
            import_ed25519_privatekey_from_file(fn_prompt, prompt=True)
            import_ed25519_privatekey_from_file(fn_encrypted, prompt=True)

        # ... and the passed pw.
        import_ed25519_privatekey_from_file(fn_prompt, password=pw)
        import_ed25519_privatekey_from_file(fn_encrypted, password=pw)

        # TEST: Import existing keys with encrypted private key (test regression)
        # Assert format
        pub = import_ed25519_publickey_from_file(self.path_ed25519 + ".pub")
        priv = import_ed25519_privatekey_from_file(
            self.path_ed25519, "password"
        )

        self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub))
        self.assertTrue(ED25519KEY_SCHEMA.matches(pub))
        self.assertTrue(ED25519KEY_SCHEMA.matches(priv))
        # NOTE: There is no private key schema, at least check it has a value
        self.assertTrue(priv["keyval"]["private"])

        # TEST: Unexpected behavior
        # FIXME: Should 'import_ed25519_publickey_from_file' be able to import a
        # a non-encrypted ed25519 private key? I think it should not, but it is:
        priv = import_ed25519_publickey_from_file(fn_default)
        self.assertTrue(ED25519KEY_SCHEMA.matches(priv))
        self.assertTrue(priv["keyval"]["private"])

        # FIXME: Should 'import_ed25519_privatekey_from_file' be able to import a
        # an ed25519 public key? I think it should not, but it is:
        pub = import_ed25519_privatekey_from_file(fn_default + ".pub")
        self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub))

        # TEST: Generation errors
        for idx, (kwargs, err_msg) in enumerate(
            [
                # Error on empty password
                (
                    {"password": ""},
                    "encryption password must be 1 or more characters long",
                ),
                # Error on 'password' and 'prompt=True'
                (
                    {"password": pw, "prompt": True},
                    "passing 'password' and 'prompt=True' is not allowed",
                ),
            ]
        ):

            with self.assertRaises(
                ValueError,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ) as ctx:
                _generate_and_write_ed25519_keypair(**kwargs)

            self.assertEqual(
                err_msg,
                str(ctx.exception),
                "expected: '{}' got: '{}' (row {})".format(  # pylint: disable=consider-using-f-string
                    err_msg, ctx.exception, idx
                ),
            )

        # Error on bad argument format
        for idx, kwargs in enumerate(
            [
                {"filepath": 123456},  # Not a string
                {"password": 123456},  # Not a string
                {"prompt": "not-a-bool"},
            ]
        ):
            with self.assertRaises(
                FormatError,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ):
                _generate_and_write_ed25519_keypair(**kwargs)

        # TEST: Import errors
        # Error on public key import...
        for idx, (fn, err_msg) in enumerate(
            [
                # Error on invalid json (custom key format)
                (fn_encrypted, "Cannot deserialize to a Python object"),
                # Error on invalid custom key format
                (self.path_no_key, "Missing key"),
                # Error on invalid key type
                (self.path_ecdsa + ".pub", "Invalid key type loaded"),
            ]
        ):
            with self.assertRaises(
                Error,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ) as ctx:
                import_ed25519_publickey_from_file(fn)

            self.assertTrue(
                err_msg in str(ctx.exception),
                "expected: '{}' got: '{}' (row {})".format(  # pylint: disable=consider-using-f-string
                    err_msg, ctx.exception, idx
                ),
            )

        # Error on private key import...
        for idx, (args, kwargs, err, err_msg) in enumerate(
            [
                # Error on not an ed25519 private key
                (
                    [self.path_ecdsa],
                    {},
                    CryptoError,
                    "Malformed Ed25519 key JSON, possibly due to encryption, "
                    "but no password provided?",
                ),
                # Error on not encrypted
                (
                    [fn_default],
                    {"password": pw},
                    CryptoError,
                    "Invalid encrypted file.",
                ),
                # Error on encrypted but no pw
                (
                    [fn_encrypted],
                    {},
                    CryptoError,
                    "Malformed Ed25519 key JSON, possibly due to encryption, "
                    "but no password provided?",
                ),
                # Error on encrypted but empty pw
                (
                    [fn_encrypted],
                    {"password": ""},
                    CryptoError,
                    "Decryption failed.",
                ),
                # Error on encrypted but bad pw passed
                (
                    [fn_encrypted],
                    {"password": "bad pw"},
                    CryptoError,
                    "Decryption failed.",
                ),
                # Error on pw and prompt
                (
                    [fn_default],
                    {"password": pw, "prompt": True},
                    ValueError,
                    "passing 'password' and 'prompt=True' is not allowed",
                ),
            ]
        ):

            with self.assertRaises(
                err,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ) as ctx:
                import_ed25519_privatekey_from_file(*args, **kwargs)

            self.assertTrue(
                err_msg in str(ctx.exception),
                "expected: '{}' got: '{}' (row {})".format(  # pylint: disable=consider-using-f-string
                    err_msg, ctx.exception, idx
                ),
            )

        # Error on encrypted but bad pw prompted
        err_msg = (
            "Malformed Ed25519 key JSON, possibly due to encryption, "
            "but no password provided?"
        )
        with self.assertRaises(CryptoError) as ctx, mock.patch(
            "securesystemslib.interface.get_password", return_value="bad_pw"
        ):
            import_ed25519_privatekey_from_file(fn_encrypted)

        self.assertTrue(
            err_msg in str(ctx.exception),
            "expected: '{}' got: '{}'".format(  # pylint: disable=consider-using-f-string
                err_msg, ctx.exception
            ),
        )

        # Error on bad path format
        with self.assertRaises(FormatError):
            import_ed25519_publickey_from_file(123456)
        with self.assertRaises(FormatError):
            import_ed25519_privatekey_from_file(123456)

        # Error on bad password format
        with self.assertRaises(FormatError):
            import_ed25519_privatekey_from_file(fn_default, password=123456)

        # Error on bad prompt format
        with self.assertRaises(FormatError):
            import_ed25519_privatekey_from_file(fn_default, prompt="not-a-bool")

    def test_ecdsa(self):  # pylint: disable=too-many-locals,too-many-statements
        """Test ecdsa key _generation and import interface functions."""
        # TEST: Generate default keys and import
        # Assert location and format
        fn_default = "default"
        fn_default_ret = _generate_and_write_ecdsa_keypair(filepath=fn_default)

        pub = import_ecdsa_publickey_from_file(fn_default + ".pub")
        priv = import_ecdsa_privatekey_from_file(fn_default)

        self.assertEqual(fn_default, fn_default_ret)
        self.assertTrue(ECDSAKEY_SCHEMA.matches(pub))
        self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub))
        self.assertTrue(ECDSAKEY_SCHEMA.matches(priv))
        # NOTE: There is no private key schema, at least check it has a value
        self.assertTrue(priv["keyval"]["private"])

        # TEST: Generate unencrypted keys with empty prompt
        # Assert importable with empty prompt password and without password
        fn_empty_prompt = "empty_prompt"
        with mock.patch(
            "securesystemslib.interface.get_password", return_value=""
        ):
            _generate_and_write_ecdsa_keypair(filepath=fn_empty_prompt)
            import_ecdsa_privatekey_from_file(fn_empty_prompt, prompt=True)
        import_ecdsa_privatekey_from_file(fn_empty_prompt)

        # TEST: Generate keys with auto-filename, i.e. keyid
        # Assert filename is keyid
        fn_keyid = _generate_and_write_ecdsa_keypair()
        pub = import_ecdsa_publickey_from_file(fn_keyid + ".pub")
        priv = import_ecdsa_privatekey_from_file(fn_keyid)
        self.assertTrue(
            os.path.basename(fn_keyid) == pub["keyid"] == priv["keyid"]
        )

        # TEST: Generate two key pairs with encrypted private keys using ...
        pw = "pw"
        fn_encrypted = "encrypted"
        fn_prompt = "prompt"
        # ...  a passed pw ...
        _generate_and_write_ecdsa_keypair(filepath=fn_encrypted, password=pw)
        with mock.patch(
            "securesystemslib.interface.get_password", return_value=pw
        ):
            # ... and a prompted pw.
            _generate_and_write_ecdsa_keypair(filepath=fn_prompt, prompt=True)

            # Assert that both private keys are importable using the prompted pw ...
            import_ecdsa_privatekey_from_file(fn_prompt, prompt=True)
            import_ecdsa_privatekey_from_file(fn_encrypted, prompt=True)

        # ... and the passed pw.
        import_ecdsa_privatekey_from_file(fn_prompt, password=pw)
        import_ecdsa_privatekey_from_file(fn_encrypted, password=pw)

        # TEST: Import existing keys with encrypted private key (test regression)
        # Assert format
        pub = import_ecdsa_publickey_from_file(self.path_ecdsa + ".pub")
        priv = import_ecdsa_privatekey_from_file(self.path_ecdsa, "password")

        self.assertTrue(ECDSAKEY_SCHEMA.matches(pub))
        self.assertTrue(PUBLIC_KEY_SCHEMA.matches(pub))
        self.assertTrue(ECDSAKEY_SCHEMA.matches(priv))
        # NOTE: There is no private key schema, at least check it has a value
        self.assertTrue(priv["keyval"]["private"])

        # FIXME: Should 'import_ecdsa_publickey_from_file' be able to import a
        # an ed25519 public key? I think it should not, but it is:
        import_ecdsa_publickey_from_file(self.path_ed25519 + ".pub")
        self.assertTrue(ECDSAKEY_SCHEMA.matches(pub))

        # TEST: Generation errors
        for idx, (kwargs, err_msg) in enumerate(
            [
                # Error on empty password
                (
                    {"password": ""},
                    "encryption password must be 1 or more characters long",
                ),
                # Error on 'password' and 'prompt=True'
                (
                    {"password": pw, "prompt": True},
                    "passing 'password' and 'prompt=True' is not allowed",
                ),
            ]
        ):

            with self.assertRaises(
                ValueError,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ) as ctx:
                _generate_and_write_ecdsa_keypair(**kwargs)

            self.assertEqual(
                err_msg,
                str(ctx.exception),
                "expected: '{}' got: '{}' (row {})".format(  # pylint: disable=consider-using-f-string
                    err_msg, ctx.exception, idx
                ),
            )

        # Error on bad argument format
        for idx, kwargs in enumerate(
            [
                {"filepath": 123456},  # Not a string
                {"password": 123456},  # Not a string
                {"prompt": "not-a-bool"},
            ]
        ):
            with self.assertRaises(
                FormatError,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ):
                _generate_and_write_ecdsa_keypair(**kwargs)

        # TEST: Import errors

        # Error on public key import...
        for idx, (fn, err_msg) in enumerate(
            [
                # Error on invalid json (custom key format)
                (fn_encrypted, "Cannot deserialize to a Python object"),
                # Error on invalid custom key format
                (self.path_no_key, "Missing key"),
            ]
        ):
            with self.assertRaises(
                Error,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ) as ctx:
                import_ecdsa_publickey_from_file(fn)

            self.assertTrue(
                err_msg in str(ctx.exception),
                "expected: '{}' got: '{}' (row {})".format(  # pylint: disable=consider-using-f-string
                    err_msg, ctx.exception, idx
                ),
            )

        # Error on private key import...
        for idx, (args, kwargs, err, err_msg) in enumerate(
            [
                # Error on not an ecdsa private key
                (
                    [self.path_ed25519],
                    {},
                    Error,
                    "Cannot deserialize to a Python object",
                ),
                # Error on not encrypted
                (
                    [fn_default],
                    {"password": pw},
                    CryptoError,
                    "Invalid encrypted file.",
                ),
                # Error on encrypted but no pw
                (
                    [fn_encrypted],
                    {},
                    Error,
                    "Cannot deserialize to a Python object",
                ),
                # Error on encrypted but empty pw
                (
                    [fn_encrypted],
                    {"password": ""},
                    CryptoError,
                    "Decryption failed.",
                ),
                # Error on encrypted but bad pw passed
                (
                    [fn_encrypted],
                    {"password": "bad pw"},
                    CryptoError,
                    "Decryption failed.",
                ),
                # Error on pw and prompt
                (
                    [fn_default],
                    {"password": pw, "prompt": True},
                    ValueError,
                    "passing 'password' and 'prompt=True' is not allowed",
                ),
            ]
        ):

            with self.assertRaises(
                err,
                msg="(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            ) as ctx:
                import_ecdsa_privatekey_from_file(*args, **kwargs)

            self.assertTrue(
                err_msg in str(ctx.exception),
                "expected: '{}' got: '{}' (row {})".format(  # pylint: disable=consider-using-f-string
                    err_msg, ctx.exception, idx
                ),
            )

        # Error on encrypted but bad pw prompted
        err_msg = "Decryption failed"
        with self.assertRaises(CryptoError) as ctx, mock.patch(
            "securesystemslib.interface.get_password", return_value="bad_pw"
        ):
            import_ecdsa_privatekey_from_file(fn_encrypted, prompt=True)

        self.assertTrue(
            err_msg in str(ctx.exception),
            "expected: '{}' got: '{}'".format(  # pylint: disable=consider-using-f-string
                err_msg, ctx.exception
            ),
        )

        # Error on bad path format
        with self.assertRaises(FormatError):
            import_ecdsa_publickey_from_file(123456)
        with self.assertRaises(FormatError):
            import_ecdsa_privatekey_from_file(123456)

        # Error on bad password format
        with self.assertRaises(FormatError):  # bad password
            import_ecdsa_privatekey_from_file(fn_default, password=123456)

        # Error on bad prompt format
        with self.assertRaises(FormatError):
            import_ecdsa_privatekey_from_file(fn_default, prompt="not-a-bool")

    def test_generate_keypair_wrappers(self):
        """Basic tests for thin wrappers around _generate_and_write_*_keypair.
        See 'test_rsa', 'test_ed25519' and 'test_ecdsa' for more thorough key
        generation tests for each key type.

        """
        key_pw = "pw"
        expected_priv_mode = stat.S_IFREG | stat.S_IRUSR | stat.S_IWUSR
        for idx, (gen, gen_prompt, gen_plain, import_priv, schema) in enumerate(
            [
                (
                    generate_and_write_rsa_keypair,
                    generate_and_write_rsa_keypair_with_prompt,
                    generate_and_write_unencrypted_rsa_keypair,
                    import_rsa_privatekey_from_file,
                    RSAKEY_SCHEMA,
                ),
                (
                    generate_and_write_ed25519_keypair,
                    generate_and_write_ed25519_keypair_with_prompt,
                    generate_and_write_unencrypted_ed25519_keypair,
                    import_ed25519_privatekey_from_file,
                    ED25519KEY_SCHEMA,
                ),
                (
                    generate_and_write_ecdsa_keypair,
                    generate_and_write_ecdsa_keypair_with_prompt,
                    generate_and_write_unencrypted_ecdsa_keypair,
                    import_ecdsa_privatekey_from_file,
                    ECDSAKEY_SCHEMA,
                ),
            ]
        ):

            assert_msg = (
                "(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                )
            )
            # Test generate_and_write_*_keypair creates an encrypted private key
            fn_encrypted = gen(key_pw)
            priv = import_priv(fn_encrypted, key_pw)
            self.assertTrue(schema.matches(priv), assert_msg)

            # Test that encrypted private key is generated with read and write
            # permissions for user only
            self.assertEqual(os.stat(fn_encrypted).st_mode, expected_priv_mode)

            # Test generate_and_write_*_keypair errors if password is None or empty
            with self.assertRaises(FormatError, msg=assert_msg):
                fn_encrypted = gen(None)
            with self.assertRaises(ValueError, msg=assert_msg):
                fn_encrypted = gen("")

            # Test generate_and_write_*_keypair_with_prompt creates encrypted private
            # key
            with mock.patch(
                "securesystemslib.interface.get_password", return_value=key_pw
            ):
                fn_prompt = gen_prompt()
            priv = import_priv(fn_prompt, key_pw)
            self.assertTrue(schema.matches(priv), assert_msg)

            # Test generate_and_write_*_keypair_with_prompt creates unencrypted
            # private key if no password is entered
            with mock.patch(
                "securesystemslib.interface.get_password", return_value=""
            ):
                fn_empty_prompt = gen_prompt()
            priv = import_priv(fn_empty_prompt)
            self.assertTrue(schema.matches(priv), assert_msg)

            # Test generate_and_write_unencrypted_*_keypair doesn't encrypt
            fn_unencrypted = gen_plain()
            priv = import_priv(fn_unencrypted)
            self.assertTrue(schema.matches(priv), assert_msg)

            # Test that unencrypted private key is generated with read and write
            # permissions for user only
            self.assertEqual(
                os.stat(fn_unencrypted).st_mode, expected_priv_mode
            )

    def test_import_publickeys_from_file(self):
        """Test import multiple public keys with different types."""

        # Successfully import key dict with one key per supported key type
        key_dict = import_publickeys_from_file(
            [
                self.path_rsa + ".pub",
                self.path_ed25519 + ".pub",
                self.path_ecdsa + ".pub",
            ],
            [KEY_TYPE_RSA, KEY_TYPE_ED25519, KEY_TYPE_ECDSA],
        )

        ANY_PUBKEY_DICT_SCHEMA.check_match(key_dict)
        self.assertListEqual(
            sorted([key["keytype"] for key in key_dict.values()]),
            sorted([KEY_TYPE_RSA, KEY_TYPE_ED25519, KEY_TYPE_ECDSA]),
        )

        # Successfully import default rsa key
        key_dict = import_publickeys_from_file([self.path_rsa + ".pub"])
        ANY_PUBKEY_DICT_SCHEMA.check_match(key_dict)
        RSAKEY_SCHEMA.check_match(list(key_dict.values()).pop())

        # Bad default rsa key type for ed25519
        with self.assertRaises(Error):
            import_publickeys_from_file([self.path_ed25519 + ".pub"])

        # Bad ed25519 key type for rsa key
        with self.assertRaises(Error):
            import_publickeys_from_file(
                [self.path_rsa + ".pub"], [KEY_TYPE_ED25519]
            )

        # Unsupported key type
        with self.assertRaises(FormatError):
            import_publickeys_from_file(
                [self.path_ed25519 + ".pub"], ["KEY_TYPE_UNSUPPORTED"]
            )

        # Mismatching arguments lists lenghts
        with self.assertRaises(FormatError):
            import_publickeys_from_file(
                [self.path_rsa + ".pub", self.path_ed25519 + ".pub"],
                [KEY_TYPE_ED25519],
            )

    def test_import_privatekey_from_file(self):
        """Test generic private key import function."""

        pw = "password"
        for idx, (path, key_type, key_schema) in enumerate(
            [
                (self.path_rsa, None, RSAKEY_SCHEMA),  # default key type
                (self.path_rsa, KEY_TYPE_RSA, RSAKEY_SCHEMA),
                (self.path_ed25519, KEY_TYPE_ED25519, ED25519KEY_SCHEMA),
                (self.path_ecdsa, KEY_TYPE_ECDSA, ECDSAKEY_SCHEMA),
            ]
        ):

            # Successfully import key per supported type, with ...
            # ... passed password
            key = import_privatekey_from_file(
                path, key_type=key_type, password=pw
            )
            self.assertTrue(
                key_schema.matches(key),
                "(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            )

            # ... entered password on mock-prompt
            with mock.patch(
                "securesystemslib.interface.get_password", return_value=pw
            ):
                key = import_privatekey_from_file(
                    path, key_type=key_type, prompt=True
                )
            self.assertTrue(
                key_schema.matches(key),
                "(row {})".format(  # pylint: disable=consider-using-f-string
                    idx
                ),
            )

        # Error on wrong key for default key type
        with self.assertRaises(Error):
            import_privatekey_from_file(self.path_ed25519, password=pw)

        # Error on unsupported key type
        with self.assertRaises(FormatError):
            import_privatekey_from_file(
                self.path_rsa, key_type="KEY_TYPE_UNSUPPORTED", password=pw
            )


# Run the test cases.
if __name__ == "__main__":
    unittest.main()
