Skip to content

Commit

Permalink
addressed review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
MaheshGSLAB committed Jun 20, 2024
1 parent 367d920 commit 3e7deab
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 80 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ repos:
language: python
files: python-avd/pyavd/_eos_cli_config_gen/j2templates/
pass_filenames: false
additional_dependencies: ['Jinja2>=3.0.0', 'deepmerge>=1.1.0', 'PyYAML>=6.0.0', 'pydantic>=2.3.0', 'jsonschema>=4.10.3', 'referencing>=0.35.0']
additional_dependencies: ['Jinja2>=3.0.0', 'deepmerge>=1.1.0', 'PyYAML>=6.0.0', 'pydantic>=2.3.0', 'jsonschema>=4.10.3', 'referencing>=0.35.0', 'cryptography>=38.0.4']
# additional_dependencies: ['Jinja2>=3.0.0']

- repo: https://github.com/igorshubovych/markdownlint-cli
Expand Down
171 changes: 126 additions & 45 deletions python-avd/pyavd/_utils/password_utils/password.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@

def _validate_password_and_key(password: str, key: str) -> None:
"""
Validates the password and key values
Validates the password and key values.
Raises ValueError if one is missing
Raises TypeError if Password is not a string
Args:
password (str): The password to validate.
key (str): The key to validate.
Raises:
ValueError: If the key or password is missing.
TypeError: If the password is not of type `str`.
"""
if not key:
raise ValueError("Key is required for encryption")
Expand All @@ -33,13 +38,18 @@ def _validate_password_and_key(password: str, key: str) -> None:
##############
def ospf_simple_encrypt(password: str, key: str) -> str:
"""
Encrypt a password for OSPF simple authentication
Encrypt a password for OSPF simple authentication.
<key> should be the interface name e.g. "Ethernet1"
Args:
password (str): The password to be encrypted.
key (str): The interface name, e.g., "Ethernet1".
The key is transformed to : <key>_passwd for simple authentication
Returns:
str: The encrypted password as a base64-encoded string.
Returns the encrypted password as a string
Raises:
ValueError: If the key or password is missing.
TypeError: If the password is not of type `str`.
"""
_validate_password_and_key(password, key)

Expand All @@ -51,15 +61,19 @@ def ospf_simple_encrypt(password: str, key: str) -> str:

def ospf_simple_decrypt(password: str, key: str) -> str:
"""
Decrypt a password for OSPF simple authentication
<key> should be the interface name e.g. "Ethernet1"
Decrypt a password for OSPF simple authentication.
The key is transformed to either: <key>_passwd for simple authentication
Args:
password (str): The encrypted password to be decrypted.
key (str): The interface name, e.g., "Ethernet1".
Returns the decrypted password as a string
Returns:
str: The decrypted password as a string.
Raises ValueError if decryption fails
Raises:
ValueError: If decryption fails.
ValueError: If the key or password is missing.
TypeError: If the password is not of type `str`.
"""
_validate_password_and_key(password, key)

Expand All @@ -77,15 +91,22 @@ def ospf_simple_decrypt(password: str, key: str) -> str:

def ospf_message_digest_encrypt(password: str, key: str, hash_algorithm: str = None, key_id: str = None) -> str:
"""
Encrypt a password for Message Digest Keys
<key> should be the interface name e.g. "Ethernet1"
<hash_algorithm> MUST be in ["md5", "sha1", "sha256", "sha384", "sha512"]
<key_id> MUST be set
Encrypt a password for Message Digest Keys.
The key is transformed to either:: <key>_<hash_algorithm>Key_<key_id> for message digest keys
Returns the encrypted password as a string
Args:
password (str): The password to be encrypted.
key (str): The interface name, e.g., "Ethernet1".
hash_algorithm (str, optional): The hash algorithm to use. Must be one of ["md5", "sha1", "sha256", "sha384", "sha512"].
key_id (str, optional): The key ID to use.
Returns:
str: The encrypted password as a base64-encoded string.
Raises:
ValueError: If `hash_algorithm` or `key_id` is not provided.
ValueError: If `hash_algorithm` is not one of the allowed values.
ValueError: If the key or password is missing.
TypeError: If the password is not of type `str`.
"""
_validate_password_and_key(password, key)
if hash_algorithm is None or key_id is None:
Expand All @@ -101,17 +122,23 @@ def ospf_message_digest_encrypt(password: str, key: str, hash_algorithm: str = N

def ospf_message_digest_decrypt(password: str, key: str, hash_algorithm: str = None, key_id: str = None) -> str:
"""
Decrypt a password for Message Digest Keys
Decrypt a password for Message Digest Keys.
<key> should be the interface name e.g. "Ethernet1"
<hash_algorithm> MUST be in ["md5", "sha1", "sha256", "sha384", "sha512"]
<key_id> MUST be set
The key is transformed to either:: <key>_<hash_algorithm>Key_<key_id> for message digest keys
Returns the decrypted password as a string
Raises ValueError if decryption fails
Args:
password (str): The encrypted password to be decrypted.
key (str): The interface name, e.g., "Ethernet1".
hash_algorithm (str, optional): The hash algorithm used for encryption. Must be one of ["md5", "sha1", "sha256", "sha384", "sha512"].
key_id (str, optional): The key ID used for encryption.
Returns:
str: The decrypted password as a string.
Raises:
ValueError: If `hash_algorithm` or `key_id` is not provided.
ValueError: If `hash_algorithm` is not one of the allowed values.
ValueError: If decryption fails.
ValueError: If the key or password is missing.
TypeError: If the password is not of type `str`.
"""
_validate_password_and_key(password, key)
if hash_algorithm is None or key_id is None:
Expand All @@ -133,9 +160,18 @@ def ospf_message_digest_decrypt(password: str, key: str, hash_algorithm: str = N
##############
def bgp_encrypt(password: str, key) -> str:
"""
Encrypt a password. The key is either <PEER_GROUP_NAME>_passwd or <NEIGHBOR_IP>_passwd
Encrypts a password for BGP (Border Gateway Protocol) authentication.
Args:
password (str): The password to be encrypted.
key (str): The key used for encryption, derived from either <PEER_GROUP_NAME> or <NEIGHBOR_IP>.
Returns:
str: The encrypted password as a base64-encoded string.
Returns the encrypted password as a string
Raises:
ValueError: If the key or password is missing.
TypeError: If the password is not of type `str`.
"""
_validate_password_and_key(password, key)

Expand All @@ -147,11 +183,19 @@ def bgp_encrypt(password: str, key) -> str:

def bgp_decrypt(password: str, key) -> str:
"""
Decrypt a password. The key is either <PEER_GROUP_NAME>_passwd or <NEIGHBOR_IP>_passwd
Decrypts a password for BGP (Border Gateway Protocol) authentication.
Returns the decrypted password as a string
Args:
password (str): The encrypted password to be decrypted.
key (str): The key used for decryption, derived from either <PEER_GROUP_NAME> or <NEIGHBOR_IP>.
Returns:
str: The decrypted password as a string.
Raises ValueError if decryption fails
Raises:
ValueError: If decryption fails.
ValueError: If the key or password is missing.
TypeError: If the password is not of type `str`.
"""
_validate_password_and_key(password, key)

Expand Down Expand Up @@ -181,6 +225,21 @@ def bgp_decrypt(password: str, key) -> str:


def _validate_isis_args(password: str, key: str, mode: str):
"""
Validates the arguments for ISIS (Intermediate System to Intermediate System) encryption/decryption.
Args:
password (str): The password to be encrypted/decrypted.
key (str): The key used for encryption/decryption.
mode (str): The mode of operation for encryption/decryption, which should be one of the options in `_ISIS_MODE_MAP`.
Raises:
ValueError: If `password` is empty or missing.
TypeError: If `password` is not of type `str`.
TypeError: If `key` is not of type `str`.
TypeError: If `mode` is not of type `str` or is not one of the valid options in `_ISIS_MODE_MAP`.
ValueError: If `mode` is empty or missing.
"""
if not password:
raise ValueError("Password is required for encryption/decryption")

Expand All @@ -198,6 +257,16 @@ def _validate_isis_args(password: str, key: str, mode: str):


def _get_isis_key(key: str, mode: str) -> bytes:
"""
Constructs a key for ISIS (Intermediate System to Intermediate System) encryption/decryption.
Args:
key (str): The base key string.
mode (str): The mode of operation, which determines how the key is formatted.
Returns:
bytes: The constructed key as bytes.
"""
return bytes(f"{key}_{_ISIS_MODE_MAP[mode]}", encoding="UTF-8")


Expand All @@ -210,7 +279,8 @@ def isis_encrypt(password: str, key: str, mode: str) -> str:
key: ISIS instance name as string.
mode: 'none', 'text', 'md5' or 'sha' or for shared-secret mode 'sha-1', 'sha-224', 'sha-256', 'sha-384', 'sha-512'.
Returns the encrypted password as a string.
Returns:
str: The encrypted password as a string.
"""
_validate_isis_args(password, key, mode)

Expand All @@ -223,12 +293,21 @@ def isis_decrypt(password: str, key: str, mode: str) -> str:
"""
Decrypt a password for ISIS authentication.
<key> ISIS instance name.
<mode> 'none', 'text', 'md5' or 'sha' or for shared-secret mode 'sha-1', 'sha-224', 'sha-256', 'sha-384', 'sha-512'.
Returns the decrypted password as a string.
Raises ValueError if decryption fails
Args:
password (str): The encrypted password to be decrypted.
key (str): The ISIS instance name used to derive the decryption key.
mode (str): Specifies the decryption mode. Can be one of:
- 'none': No encryption.
- 'text': Plain text (no decryption needed, but processed accordingly).
- 'md5': MD5 hash decryption.
- 'sha': SHA-1 hash decryption.
- 'sha-1', 'sha-224', 'sha-256', 'sha-384', 'sha-512': Various SHA hash decryptions.
Returns:
str: The decrypted password as a string.
Raises:
ValueError: If decryption fails for any reason.
"""
_validate_isis_args(password, key, mode)

Expand All @@ -253,7 +332,8 @@ def simple_7_decrypt(data: str) -> str:
Args:
data: The encrypted password
Returns the decrypted password as a string.
Returns:
str: The decrypted password as a string.
"""
salt = int(data[0:2])
secret = bytearray.fromhex(data[2:])
Expand All @@ -268,7 +348,8 @@ def simple_7_encrypt(data: str, salt: int | None = None) -> str:
data: The clear text password
salt: Optionally a number within the range 0-15. If not set, a random salt will be used.
Returns the encrypted password as a string.
Returns:
str: The encrypted password as a string.
"""
if salt is None:
# Accepting SonarLint issue: Pseudo random is ok since this is simply creating a visible salt
Expand Down
48 changes: 29 additions & 19 deletions python-avd/pyavd/_utils/password_utils/password_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,8 @@

import base64

try:
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

HAS_CRYPTOGRAPHY = True
except ImportError:
HAS_CRYPTOGRAPHY = False
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

SEED = b"\xd5\xa8\xc9\x1e\xf5\xd5\x8a\x23"

Expand Down Expand Up @@ -176,10 +171,15 @@ def hashkey(pw) -> bytes:

def cbc_encrypt(key: bytes, data: bytes) -> bytes:
"""
Encrypt a password. The key is either <PEER_GROUP_NAME>_passwd or <NEIGHBOR_IP>_passwd
Encrypt a password.
Args:
key (bytes): The encryption key, which should be the peer group name or neighbor IP with '_passwd' suffix.
data (bytes): The data to be encrypted.
Returns:
bytes: The encrypted data, encoded in base64.
"""
if not HAS_CRYPTOGRAPHY:
raise ImportError("AVD could not import the required 'cryptography' Python library")

hashed_key = hashkey(key)
padding = (8 - ((len(data) + 4) % 8)) % 8
Expand All @@ -196,13 +196,18 @@ def cbc_encrypt(key: bytes, data: bytes) -> bytes:

def cbc_decrypt(key: bytes, data: bytes) -> bytes:
"""
Decrypt a password. The key is either <PEER_GROUP_NAME>_passwd or <NEIGHBOR_IP>_passwd
Decrypt a password.
Args:
key (bytes): The decryption key, which should be the peer group name or neighbor IP with '_passwd' suffix.
data (bytes): The base64-encoded data to be decrypted.
raises:
* TypeError: if the length of the provided data is not a multiple of the block length.
Returns:
bytes: The decrypted data.
Raises:
ValueError: If the decrypted data is invalid or the length of the provided data is not a multiple of the block length.
"""
if not HAS_CRYPTOGRAPHY:
raise ImportError("AVD could not import the required 'cryptography' Python library")

data = base64.b64decode(data)
hashed_key = hashkey(key)
Expand All @@ -223,12 +228,17 @@ def cbc_decrypt(key: bytes, data: bytes) -> bytes:

def cbc_check_password(key: bytes, data: bytes) -> bool:
"""
This function is used to verify if an encrypted password is decryptable.
It does not return the password but only raise an error if the password cannot be decrypted
Verify if an encrypted password is decryptable.
It does not return the password but only raises an error if the password cannot be decrypted
Args:
key (bytes): The decryption key, which should be the peer group name or neighbor IP with '_passwd' suffix.
data (bytes): The base64-encoded encrypted password data to be decrypted.
Returns:
bool: `True` if the password is decryptable, `False` otherwise.
"""
# pylint: disable=W0718
if not HAS_CRYPTOGRAPHY:
raise ImportError("AVD could not import the required 'cryptography' Python library")

try:
cbc_decrypt(key, data)
Expand Down
15 changes: 14 additions & 1 deletion python-avd/pyavd/j2filters/decrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,20 @@

def decrypt(value, passwd_type=None, key=None, **kwargs) -> str:
"""
Umbrella function to execute the correct decrypt method based on the input type
Umbrella function to execute the correct decrypt method based on the input type.
Args:
value (any): The value to decrypt.
passwd_type (str): The type of decryption method to use. Must be provided.
key (str or None): Optional decryption key or parameter.
**kwargs: Additional keyword arguments passed to the specific decryption method.
Returns:
str: The decrypted value as a string.
Raises:
TypeError: If `passwd_type` is not provided.
KeyError: If `passwd_type` is not found in `METHODS_DIR`.
"""
if not passwd_type:
raise TypeError("type keyword must be present to use this test")
Expand Down
Loading

0 comments on commit 3e7deab

Please sign in to comment.