import os import hashlib import Crypto.Cipher.AES class bad_password(Exception): """ This exception is user to mark when bad password had been used. """ pass class secret_coder: """ This class is required for crypting and decrypting secret by passwords. It store crypted secret as strings in coded form, which make it easies to store in database, and send over REST api. Format: :: - hashed_secret: SHA256 of the secret, to validate success of the decrypt. - cipher: Cipher from AES. - crypted_secret: Crypted secret to encrypt with password. """ def __init__(self, password: str) -> None: """ This functio initialize new coder. It require password, which would be used to encrypting and decrypting secrets. Parameters: password (str): Password to use as crypto key """ password = password.encode("UTF-8") hashed = hashlib.sha256(password) self.__password = hashed.digest() @property def password(self) -> bytes: """ It return password as crypto key. """ return self.__password def encrypt(self, plain: str) -> str: """ This function encrypt new secret, and return it in specified format. Parameters: plain (str): Secret to encode Returns: (str): Encrypted secret """ cipher = self.__iv coder = self.__cipher(cipher) crypted = coder.encrypt(plain.encode("UTF-8")) hashed = self.__hash(plain) return self.__pack(hashed, cipher, crypted) def decrypt(self, crypted: str) -> str: """ This function decrypt secret in given format, and return decrypted secret. When password which initialize coder is bad, then bad_password Exception had been raiser. Parameters: crypted (str): Crypted secret in coder format Returns: (str): Decrypted secret """ hashed, cipher, crypted = self.__unpack(crypted) coder = self.__cipher(cipher) result = coder.decrypt(crypted) try: result = result.decode("UTF-8") except: raise bad_password("Crypto key is not correct for this secret.") if hashed != self.__hash(result): raise bad_password("Crypto key is not correct for this secret.") return result @property def mode(self) -> int: """ This return AES mode which had been used by coder. """ return Crypto.Cipher.AES.MODE_CFB @property def __iv(self) -> bytes: """ This return new random cipher for AES. """ return os.urandom(secret_coder.__get_cipher_length()) def __get_hash_length() -> int: """ This return lenght of hash in bytes. """ return int(256 / 8) def __get_cipher_length() -> int: """ This return lenght of random cipher. """ return 16 def __cipher(self, iv: bytes) -> object: """ This return new AES coder to work with coders. """ return Crypto.Cipher.AES.new( self.password, self.mode, iv = iv ) def __hash(self, secret: str) -> bytes: """ This function hash given string, and return it as bytes. Parameters: secret (str): Content to hash Returns: (bytes): Hashed content """ secret = secret.encode("UTF-8") hashed = hashlib.sha256(secret) return hashed.digest() @property def separator(self) -> str: """ This return separator used to packing secret. """ return secret_coder.__get_separator() def __get_separator() -> str: """ This return separator, which could be used in static function. """ return ":" def __pack(self, hashed: bytes, cipher: bytes, crypted: bytes) -> str: """ This function pack full content required for secret, to coder format. Parameters: hashed (bytes): Hashed secret cipher (bytes): Ciper used in crypter crypted (bytes): Crypted secret Returns: (str): Coded crypted secret """ hashed = hashed.hex() cipher = cipher.hex() crypted = crypted.hex() return hashed + self.separator + cipher + self.separator + crypted def __unpack(self, crypted: str) -> [bytes, bytes, bytes]: """ This function unpack given coded secret into bytes parts. When secret is not valid raise TypeError. Parameters: crypted (str): Packed secret Returns: (bytes): Hashed secret before encryption to validate password (bytes): Cipher required by AES to work (bytes): Crypted secret to encrypt """ splited = crypted.split(self.separator) if len(splited) != 3: raise TypeError("Secret is in invalid format.") hashed = bytes.fromhex(splited[0]) cipher = bytes.fromhex(splited[1]) crypted = bytes.fromhex(splited[2]) if len(cipher) != secret_coder.__get_cipher_length(): raise TypeError("Secret is in invalid format.") if len(hashed) != secret_coder.__get_hash_length(): raise TypeError("Secret is in invalid format.") return hashed, cipher, crypted def validate(coded: str) -> bool: """ This validate that coded secret is correct. That check hashed lenght and cipher lenght. This is static function. Parameters: coded (str): Codec secret to check correction of Returns: (bool): True when secret is corret, False when not. """ splited = coded.split(secret_coder.__get_separator()) if len(splited) != 3: return False hashed = bytes.fromhex(splited[0]) cipher = bytes.fromhex(splited[1]) if len(cipher) != secret_coder.__get_cipher_length(): return False if len(hashed) != secret_coder.__get_hash_length(): return False return True