Procházet zdrojové kódy

Add new method to crypt secrets.

Cixo Develop před 7 měsíci
rodič
revize
ffb92954bb
3 změnil soubory, kde provedl 222 přidání a 1 odebrání
  1. 3 1
      assets/__init__.py
  2. 185 0
      assets/secret_coder.py
  3. 34 0
      tests/006-secret_coder.py

+ 3 - 1
assets/__init__.py

@@ -8,4 +8,6 @@ from .secret import secret
 from .secret import secret_builder
 from .secret_loader import secret_loader
 from .secret_crypto import secret_crypto
-from .builder import builder
+from .secret_coder import secret_coder
+from .secret_coder import bad_password
+from .builder import builder

+ 185 - 0
assets/secret_coder.py

@@ -0,0 +1,185 @@
+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>:<cipher>:<crypted_secret>
+
+     - 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(self.__cipher_length)
+
+    @property
+    def __cipher_length(self) -> 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 ":"
+
+    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) != self.__cipher_length:
+            raise TypeError("Secret is in invalid format.")
+        
+        return hashed, cipher, crypted

+ 34 - 0
tests/006-secret_coder.py

@@ -0,0 +1,34 @@
+import pathlib
+
+current = pathlib.Path(__file__).parent
+root = current.parent
+
+import sys
+sys.path.append(str(root))
+
+import assets
+
+crypter = assets.secret_coder("password")
+test_1 = crypter.encrypt("UwU")
+
+print("UwU crypted: " + test_1)
+print("\"" + test_1 + "\" decrypted: " + crypter.decrypt(test_1))
+
+print("")
+print("New crypted creating...")
+
+crypter = assets.secret_coder("password")
+
+print("\"" + test_1 + "\" decrypted: " + crypter.decrypt(test_1))
+
+print("")
+print("Testing with bad password...")
+
+crypter = assets.secret_coder("password 2")
+
+try:
+    crypter.decrypt(test_1)
+    print("Exception for bad password not working.")
+
+except assets.bad_password:
+    print("Exception for bad password working.")