Source code for credentials

import os
import sys
import re
from getpass import getpass
from pathlib import Path
import base64
from Crypto.Cipher import AES
# conquers libraries
from constants import Constants as Const

[docs] class MasterkeyError(Exception): pass
[docs] class CredentialsError(Exception): pass
[docs] class CryptoError(Exception): pass
[docs] class Credentials: """ A class to check, retrieve, add, encrypt and decrypt credentials necessary to connect to a switch. Attributes: credentials_file (str) Relative or absolute path to a credentials file that is to be used. It is passed via the command line flag ``--credentials``. If no path is passed, the default credentials file `credentials` in :py:class:`~constants.Const.CHOME` is used. file_handle (file) File handle used to read from and write to credentials file. master_key (str) Used to encrypt and decrypt passwords. Can have a lenth of **32** characters max. Can be passed via the **-m** flag and can be the actual master key (discouraged) or a relative/absolute path to a file containing the master key. If not provided :py:func:`~cscredentials.Credentials.__init__` will ask for it using the python module `getpass <https://docs.python.org/3/library/getpass.html>`_. """ def __init__(self, credentials_file, master_key=None) -> None: self.credentials_file = credentials_file.replace("~", str(Path.home())) self.file_handle = None if master_key is not None: if Path( master_key.replace( "~", str(Path.home()) ) ).is_file(): with open(master_key, "r", encoding="utf-8") as mfile: master_key = mfile.readline().rstrip() try: self.check_credentials_file_exists() except CredentialsError as e: print(e) sys.exit(1) if master_key is None: for i in range(0,3): # ask three times max. try1 = getpass(prompt="Master key: ") try2 = getpass(prompt="Please repeat: ") if try1 == try2: master_key = try1 break if i == 2: print("Keys do not match, giving up.") sys.exit(1) else: print("No match, try again.") # Master key of max length of 32 characters. if len(master_key) > 32: raise CredentialsError("Master key can only be 32 characters long.") self.master_key = master_key.encode().rjust(32)
[docs] def check_credentials_file_exists(self) -> None: """ Checks if the passed credentials file exists. Uses default credentials file in :py:class:`~constants.Const.CHOME_ABS_PATH` """ if not Path(self.credentials_file).is_file(): Const.set_abs_path() print(f"Credentials file {self.credentials_file} does not exist \ using {Const.CHOME}credentials ...") self.credentials_file = f"{Const.CHOME_ABS_PATH}/credentials"
[docs] def encrypt_pass(self, credentials) -> str: """ Encrypt password with AES using :py:class:`~credentials.Credentials.master_key` and encode it using base64. """ cipher = AES.new(self.master_key, AES.MODE_EAX) ciphertext, tag = cipher.encrypt_and_digest(credentials["pass"].encode()) encrypted = b'' for x in (cipher.nonce, tag, ciphertext): encrypted += x return base64.b64encode(encrypted).decode()
[docs] def decrypt_pass(self, credentials) -> str: """ Decode base64 string and decrypt using :py:class:`~credentials.Credentials.master_key` """ decoded = base64.b64decode(credentials["encrypted_pass"]) nonce = decoded[0:16] tag = decoded[16:32] ciphertext = decoded[32:len(decoded)] cipher = AES.new(self.master_key, AES.MODE_EAX, nonce) try: data = cipher.decrypt_and_verify(ciphertext, tag) except Exception as e: raise CryptoError(e) from e return "{}".format(data.decode())
[docs] def add_entry(self, credentials) -> None: """ Writes entry to :py:class:`~credentials.Credentials.credentials_file` """ credentials["encrypted_pass"] = self.encrypt_pass(credentials) with open(self.credentials_file, "a", encoding="utf-8") \ as self.file_handle: self.file_handle.write( "{user}@{host}:{encrypted_pass}\n".format(**credentials) )
[docs] def override_entry(self, credentials) -> None: """ Override matching credentials entry with the new one. """ credentials["encrypted_pass"] = self.encrypt_pass(credentials) with open(self.credentials_file, "r", encoding="utf-8") \ as self.file_handle: lines = self.file_handle.readlines() with open(self.credentials_file, "w", encoding="utf-8") \ as self.file_handle: for l in lines: if l.split(":")[0].split("@")[1] == credentials["host"]: self.file_handle.write( "{user}@{host}:{encrypted_pass}\n".format(**credentials) ) else: self.file_handle.write(l)
[docs] def check_entry_exists(self, host) -> str: """ Returns `user@host` of the match, if found, an empty string otherwise. """ with open(self.credentials_file, "r", encoding="utf-8") as \ self.file_handle: lines = self.file_handle.readlines() for l in lines: if host == l.split(":")[0].split("@")[1]: return l.split(":")[0] return ""
[docs] def checks_add_entry(self, credentials=None) -> bool: """ Asks for * host * user * password Asks whether to override, if entry exists. Asks three times to repeat at most, if passwords do not match. Aborts if override is denied, otherwise calls :py:class:`~credentials.Credentials.override_entry()` or :py:class:`~credentials.Credentials.add_entry()` """ if credentials is None: while True: credentials = { "host": None, "user": None, "pass": None, } credentials["host"] = input("Hostname: ") credentials["user"] = input( "Username for {host}: ".format(**credentials) ) # Ask three times at most. for i in range(0,3): try1 = getpass( prompt="Password for {user}@{host}:".format( **credentials ) ) try2 = getpass(prompt="Repeat: ") if try1 == try2: credentials["pass"] = try1 break if i == 2: print("Passwords do not match, giving up.") sys.exit(1) else: print("Passwords do not match, try again.") # Ask if everything's ok. print("{user}@{host}".format(**credentials)) print("Does this seem ok? [Y/n] ", end="") yn = input() # Exit while loop if ok. if yn == "" or yn.lower() == "y": break # Check if entry already exists. if(check := self.check_entry_exists("{host}".format(**credentials))): print(f"An entry already exists ({check}). Overwrite? [y/N] ", end="") yn = input() if yn.lower() != "y": print("Entry not added.") return False # Add entry. try: self.override_entry(credentials) # Can be PermissionError or FileNotFoundError except Exception as e: print(e) sys.exit(1) # Add entry. else: try: self.add_entry(credentials) except Exception as e: print(e) sys.exit(1) print("Entry added.") return True
[docs] def return_credentials(self, host) -> dict: """ Returns credentials dictionary if entry found .. code-block:: python :caption: Something like this { "user": "username", "host": "hostname", "encrypted_pass": "base64 string of encrypted password", "pass": "plain text password" } Returns empty dictionary otherwise. """ with open(self.credentials_file, "r", encoding="utf-8") as \ self.file_handle: lines = self.file_handle.readlines() for l in lines: up = l.split(":") uh = up[0].split("@") # try to match when wildcard found. regex = None if "*" in uh[1]: regex = re.compile(uh[1]) if host == uh[1] or (regex is not None and regex.match(host)): credentials = { "user": uh[0], "host": host, "encrypted_pass": up[1].rstrip() } try: credentials["pass"] = self.decrypt_pass(credentials) except CryptoError as e: print(e) print( "Probably the master key is not correct or the path" + " (if passed)" + " does not exist." ) sys.exit(1) return credentials return {}