Bläddra i källkod

Add global crypto keys for users.

Cixo Develop 7 månader sedan
förälder
incheckning
0350581d79

+ 3 - 1
assets/__init__.py

@@ -7,7 +7,9 @@ from .apikey import apikey
 from .secret import secret
 from .secret import secret_builder
 from .secret_loader import secret_loader
-from .secret_crypto import secret_crypto
 from .secret_coder import secret_coder
 from .secret_coder import bad_password
 from .builder import builder
+from .code_key import code_key
+from .code_key import code_key_generator
+from .code_key import code_key_manager

+ 125 - 0
assets/code_key.py

@@ -0,0 +1,125 @@
+import os
+
+from .secret_coder import secret_coder
+
+class code_key_manager:
+    """
+    This class is responsible for handling users crypto keys. It could decrypt
+    code to use it in other secrets, and recrypt it to use other password.
+    """
+
+    def __init__(self, password: str, key: str) -> None:
+        """
+        Parameters:
+            password (str): Password of key, used to decrypt it
+            key (str): Encrypted key to handle it
+        """
+
+        self.__coder = secret_coder(password)
+        self.__key = key
+
+    @property
+    def encrypted(self) -> str:
+        """ This return key in encrypted form. """
+
+        return self.__key
+
+    @property
+    def decrypted(self) -> str:
+        """ This return decrypted key. """
+
+        return self.__coder.decrypt(self.__key)
+
+    def recrypt(self, new_password: str) -> object:
+        """ 
+        This return new instance of this class, but with new password, and
+        key which use this new password to encrypt themself.
+
+        Parameters:
+            new_password (str): New password to recrypt key
+
+        Returns:
+            (code_key_manager): New key handler witn new password
+        """
+        
+        coder = secret_coder(new_password)
+        encoded = coder.encrypt(self.decrypted)
+
+        return code_key_manager(new_password, encoded)
+
+class code_key_generator:
+    """
+    This class generate new random crypto key to use it for secrets 
+    encryption.
+    """
+
+    def __new__(cls, password: str) -> str:
+        """
+        This create new encryption random crypto key, which use given
+        password to encrypt itself.
+
+        Parameters:
+            password (str): Password to encrypt new crypto key
+
+        Returns:
+            (str): New crypto key encrypted with given password
+        """
+
+        coder = secret_coder(password)
+        key = cls.__random_key(cls)
+
+        return coder.encrypt(key)
+
+    def __random_key_size(cls) -> int:
+        """ 
+        This return number of bytes in random key.
+
+        Returns:
+            (int): Lenght of random key
+        """
+
+        return 128
+
+    def __random_key(cls) -> str:
+        """
+        This return random key as string. It is not encrypted, it is only
+        bare string.
+
+        Returns:
+            (str): New random key
+        """
+
+        byte_key = os.urandom(cls.__random_key_size(cls))
+        string_key = byte_key.hex()
+
+        return string_key
+
+class code_key:
+    """
+    This class is generator for code key handler. When only password is 
+    given, then random key generator is created. If also crypted key is 
+    provided, then it create full handler.
+    """
+
+    def __new__(cls, **kwargs) -> str | code_key_manager:
+        """ 
+        This is commander. It get paramaters, and return ready generated
+        encoded key, or crypto key handler.
+
+        Parameters (**kwargs):
+            password (str): Password used to encode crypto keys
+            crypted_key (str): Encrypted key, which would be handled (optional)
+
+        Returns:
+            (str): New random key, when crypted key is not provided
+            (code_key_manager): New handler, when key is provided
+        """
+
+        password = kwargs["password"]
+        
+        if not "crypted_key" in kwargs:
+            return code_key_generator(password)
+
+        key = kwargs["crypted_key"]
+
+        return code_key_manager(password, key)

+ 27 - 29
assets/secret.py

@@ -4,19 +4,22 @@ import Crypto
 
 from .user import user
 from .builder import builder
-from .secret_crypto import secret_crypto
+from .secret_coder import secret_coder
 
 class secret(sqlmodel.SQLModel, table = True):
     """
     This class represents secret in the database.
     """
 
-    id: int | None = sqlmodel.Field(default = None, primary_key = True)
-    name: str | None = sqlmodel.Field(default = None, index = True)
-    domain: str | None = sqlmodel.Field(default = None, index = True)
-    crypted: bytes | None = sqlmodel.Field(default = None)
-    nonce: bytes | None = sqlmodel.Field(default = None)
-    owner: int | None = sqlmodel.Field(default = None, foreign_key = "user.id") 
+    id: int = sqlmodel.Field(default = None, primary_key = True)
+    name: str = sqlmodel.Field(default = None, index = True)
+    domain: str = sqlmodel.Field(default = None, index = True)
+    coded: str = sqlmodel.Field(default = None, index = False)
+    owner: int = sqlmodel.Field(
+        default = None, 
+        foreign_key = "user.id", 
+        index = True
+    ) 
 
     @property
     def in_database(self) -> bool:
@@ -31,10 +34,7 @@ class secret(sqlmodel.SQLModel, table = True):
         if self.name is None or self.domain is None:
             return False
 
-        if self.crypted is None or self.nonce is None:
-            return False
-
-        if self.owner is None:
+        if self.coded is None or self.owner is None:
             return False
 
         return True
@@ -58,8 +58,7 @@ class secret(sqlmodel.SQLModel, table = True):
         result = result + "Name: " + self.name + "\n"
         result = result + "Domain: " + self.domain + "\n"
         result = result + "Owner ID: " + str(self.owner) + "\n"
-        result = result + "Crypted: " + self.crypted.hex() + "\n"
-        result = result + "Nonce: " + self.nonce.hex() + "\n"
+        result = result + "Coded: " + self.coded + "\n"
         
         return result
 
@@ -107,24 +106,23 @@ class secret_builder(builder, target_type = secret):
 
         self._target.domain = target
 
-    def crypt(self, key: str, target: str) -> None:
-        """
-        This function crypt secret. It require password which could decrypt
-        it laser, and target secret. It automatic set crypted and nonce secret
-        fields. If secret already has nonce, then it use it. When secret nonce
-        is empty, then it would provide new random nonce for secret.
+    @property
+    def coded(self) -> str | None:
+        """ This return coded secret or None. """
 
+        return self._target.coded
+
+    @coded.setter
+    def coded(self, target: str) -> None:
+        """ 
+        This set new coded secret. It also check it format, and when detect
+        that format is not correct, raise TypeError.
+        
         Parameters:
-            key (str): Password to protect secret
-            targer (str): Secret to encrypt
+            target (str): Target secret to set
         """
 
-        crypter = secret_crypto(key)
-
-        if self._target.nonce is not None:
-            crypter = crypter.set_iv(self._target.nonce)
-
-        crypted, nonce = crypter.crypted(target)
+        if not secret_coder.validate(target):
+            raise TypeError("Coded secret is not in correct format.")
 
-        self._target.crypted = crypted
-        self._target.nonce = nonce
+        self._target.coded = target

+ 44 - 4
assets/secret_coder.py

@@ -97,10 +97,14 @@ class secret_coder:
     def __iv(self) -> bytes:
         """ This return new random cipher for AES. """
 
-        return os.urandom(self.__cipher_length)
+        return os.urandom(secret_coder.__get_cipher_length())
 
-    @property
-    def __cipher_length(self) -> int:
+    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
@@ -135,6 +139,11 @@ class secret_coder:
     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:
@@ -179,7 +188,38 @@ class secret_coder:
         cipher = bytes.fromhex(splited[1])
         crypted = bytes.fromhex(splited[2])
 
-        if len(cipher) != self.__cipher_length:
+        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

+ 0 - 135
assets/secret_crypto.py

@@ -1,135 +0,0 @@
-import Crypto.Cipher.AES
-import hashlib
-import os
-
-class secret_crypto:
-    """
-    This class is responsible for creation and manage of the secrets crypto.
-    All of the secrets would have own IV. When new secret_crypto is creating,
-    IV is random. It could be change by function set_iv. Set IV is necessary
-    to decrypt secret.
-
-    Password HAVE NOT being stored in database. It must be given from API, 
-    to protect database. Password from the user is hashed by SHA256 to use it 
-    as key. IV is random, and other for all of the keys.
-    
-    If database leaks, secrets are save, because keys is provided from user
-    every request for the secret.
-    """
-
-    def __init__(
-        self, 
-        password: str, 
-        __iv: bytes | None = None,
-        __key: bytes | None = None
-    ) -> None:
-        """
-        This function create new secret_crypto. It could be used encrypt new
-        secret. To decrypt secret, app must set IV.
-
-        password: str - Password to encrypt secrets
-        __iv: bytes | None - IV, used to clone (default: None)
-        __key: bytes | None - Key, used to clone (default: None)
-        """
-
-        if __key is None:
-            encoded = password.encode("UTF-8")
-            hashed = hashlib.sha256(encoded)
-            __key = hashed.digest()
-
-        if __iv is None:
-            __iv = os.urandom(16)
-
-        self.__iv = __iv
-        self.__key = __key
-        self.__password = password
-
-    def encrypt(self, secret: str) -> bytes:
-        """
-        This function encrypt secret, using previously loaded password as key
-        and IV. IV would be random, and own for all secrets.
-
-        secret: str - Secret to encode
-        return: bytes - Encrypted secret
-        """
-
-        return self.__cipher.encrypt(secret.encode("UTF-8"))
-
-    def decrypt(self, crypted: bytes) -> str:
-        """
-        This function decrypt previously crypted secret using given password
-        as key. IV must be restore. When random IV is used, then cipher would
-        not decrypt secret.
-        
-        crypted: bytes - Crypted secret to decrypt
-        return: str - Decrypted secret
-        """
-
-        return self.__cipher.decrypt(crypted).decode("UTF-8")
-
-    def crypted(self, secret: str) -> [bytes, bytes]:
-        """
-        This function is complex version of encrypt. It do exacly the same, 
-        but it return also IV, which could make code look better.
-
-        secret: str - Secret to encrypt
-        return: [bytes, bytes] - [Encrypted secret, IV]
-        """
-
-        return self.encrypt(secret), self.iv
-
-    @property
-    def password(self) -> str:
-        """
-        return: str - Password to encrypt
-        """
-
-        return self.__password
-
-    @property
-    def key(self) -> bytes:
-        """
-        return: bytes - Password as 256 bit binary key
-        """
-
-        return self.__key
-
-    @property
-    def iv(self) -> bytes:
-        """
-        return: bytes - IV of the crypto, get it to save in database
-        """
-
-        return self.__iv
-
-    def set_iv(self, iv: bytes) -> object:
-        """
-        This function set IV. IV must be save in database, to decrypt secret
-        later. This function is used to restore IV, previously read from 
-        database.
-
-        iv: bytes - IV read from database
-        return: secret_crypto - Clone of crypto with set given iv
-        """
-
-        return secret_crypto(self.password, iv, self.key)
-
-    @property
-    def mode(self) -> int:
-        """
-        return: int - Mode of the AES used to crypt
-        """
-
-        return Crypto.Cipher.AES.MODE_CFB
-
-    @property
-    def __cipher(self) -> object:
-        """
-        return: object - New clean cipher, which could be used to work
-        """
-
-        return Crypto.Cipher.AES.new(
-            self.key, 
-            self.mode, 
-            iv = self.iv
-        )

+ 5 - 0
assets/secret_loader.py

@@ -6,6 +6,11 @@ from .user import user
 from .secret import secret
 
 class secret_loader(sqlmodel.Session):
+    """
+    This class is responsible for loading and managing secrets in the
+    database.
+    """
+
     def __init__(
         self, 
         connection: sqlalchemy.engine.base.Engine, 

+ 84 - 4
assets/user.py

@@ -3,6 +3,8 @@ import sqlmodel
 from .apikey import apikey
 from .password import password
 from .builder import builder
+from .code_key import code_key
+from .code_key import code_key_manager
 
 class user(sqlmodel.SQLModel, table = True):
     """
@@ -11,10 +13,29 @@ class user(sqlmodel.SQLModel, table = True):
     used to access database via API.
     """
 
-    id: int | None = sqlmodel.Field(default = None, primary_key = True)
+    id: int = sqlmodel.Field(default = None, primary_key = True)
     nick: str = sqlmodel.Field(index = True, unique = True)
     password: str = sqlmodel.Field(index = False)
     apikey: str = sqlmodel.Field(index = True, unique = True)
+    code_key: str = sqlmodel.Field(index = False)
+
+    def key(self, password: str) -> code_key_manager | None:
+        """
+        This return crypto key wrapper, to manage, recrypt, encrypt, decrypt
+        and other with key. It require password, and when code key is not 
+        set, return None.
+
+        Parameters:
+            password (str): Password used to decrypt crypto key
+
+        Returns:
+            (code_key_manager): Crypto key wrapper
+        """
+
+        if self.code_key is None:
+            return None
+
+        return code_key(password = password, crypted_key = self.code_key)
 
     @property
     def in_database(self) -> bool:
@@ -35,6 +56,9 @@ class user(sqlmodel.SQLModel, table = True):
         if self.apikey is None:
             return False
 
+        if self.code_key is None:
+            return False
+
         return True
 
     def __str__(self) -> str:
@@ -56,6 +80,7 @@ class user(sqlmodel.SQLModel, table = True):
         result = result + "Nick: " + self.nick + "\n"
         result = result + "Password: " + self.password + "\n"
         result = result + "API key: " + self.apikey + "\n"
+        result = result + "Code KEY: " + self.code_key + "\n"
 
         return result
 
@@ -100,10 +125,65 @@ class user_builder(builder, target_type = user):
 
         self._target.nick = target.upper()
 
-    @password.setter
-    def password(self, target: str) -> None:
+    def set_password(
+        self, 
+        password: str, 
+        old_password: str | None = None
+    ) -> None:
         """
-        target: str - New password to hash and set
+        This function set password to user. When only password is given, then 
+        it try to init user, which not have password and crypto key yet. User
+        which already set password, and crypto key, must being updated with
+        also old password.
+
+        Parameters:
+            password (str): New password to set
+            old_password (str | None) = None: Old password, require to recrypt
         """
 
+        if old_password is None:
+            self.__init_password(password)
+            return
+
+        self.__change_password(old_password, password)
+
+    def __init_password(self, target: str) -> None:
+        """
+        This function initialize user with new password. User can not already 
+        have both password, and crypto key. When user already have crypto key
+        or password, then Exception is raised, to protect crypto key to not
+        being overwrited.
+
+        Parameters:
+            target (str): New password to set
+        """
+
+        if self._target.password is not None:
+            raise Exception("Password and code key is already set.")
+
+        if self._target.code_key is not None:
+            raise Exception("Password and code key is already set.")
+
         self._target.password = password(target).result
+        self._target.code_key = code_key(password = target)
+
+    def __change_password(self, old_password: str, new_password: str) -> None:
+        """
+        This change password, when user already have password and code key. It
+        recrypt crypto key, to could use new password for secret decrypting.
+
+        Parameters:
+            old_password (str): Old password
+            new_password (str): New password to set
+        """
+
+        if old_password == new_password:
+            raise Exception("New password is same as old password.")
+
+        key = self._target.key(old_password)
+
+        if key is None or self._target.password is None:
+            raise Exception("User crypto key is not initialized yet.")
+
+        self._target.code_key = key.recrypt(new_password).encrypted
+        self._target.password = password(new_password).result

+ 20 - 2
tests/002-users.py

@@ -11,7 +11,7 @@ import sqlmodel
 
 builder = assets.user_builder()
 builder.nick = "test1"
-builder.password = "qwerty"
+builder.set_password("qwerty")
 
 test1 = builder.result
 
@@ -38,5 +38,23 @@ with assets.user_loader(connection) as loader:
     logged.apikey = assets.apikey()
     loader.save(logged)
     print("Logged user apikey after change: " + str(logged.apikey))
-    
+
+    print("Change password")
+
+    crypto = logged.key("qwerty").decrypted
+
+    helper = assets.user_builder(logged)
+    helper.set_password("QWERTY", "qwerty")
+    loader.save(helper.result)
+
+    print("Saved with new password, trying to login.")
+
+    logged = loader.login("test1", "QWERTY")
+    print("Result:" + str(logged))
+
+    if logged.key("QWERTY").decrypted == crypto:
+        print("Crypto key recrypting work.")
+    else:
+        print("Crypto key recrypting NOT WORK.")
+
 drop_database()

+ 23 - 5
tests/003-password_crypto.py

@@ -8,9 +8,27 @@ sys.path.append(str(root))
 
 import assets
 
-secret = "123456"
-sample1 = assets.secret_crypto(secret)
-sample1_crypted, sample1_iv = sample1.crypted("Sample")
+crypter = assets.secret_coder("password")
+test_1 = crypter.encrypt("UwU")
 
-sample1 = assets.secret_crypto(secret, sample1_iv)
-print("\"Sample\" after decrypt: " + sample1.decrypt(sample1_crypted))
+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.")

+ 5 - 4
tests/004-password_database.py

@@ -22,7 +22,7 @@ sqlmodel.SQLModel.metadata.create_all(connection)
 
 user_builder = assets.user_builder()
 user_builder.nick = "test"
-user_builder.password = "qwerty"
+user_builder.set_password("qwerty")
 
 sample_user = user_builder.result
 
@@ -30,12 +30,13 @@ with assets.user_loader(connection) as loader:
     loader.register(sample_user)
 
 secret_builder = assets.secret_builder()
+coder = assets.secret_coder(sample_user.key("qwerty").decrypted)
 
 secret_builder.clear()
 secret_builder.name = "secret1"
 secret_builder.owner = sample_user
 secret_builder.domain = "http://secret1"
-secret_builder.crypt("qwerty", "SAMPLE_1_PSK")
+secret_builder.coded = coder.encrypt("Sample1")
 
 secret_1 = secret_builder.result
 
@@ -43,7 +44,7 @@ secret_builder.clear()
 secret_builder.name = "secret2"
 secret_builder.owner = sample_user
 secret_builder.domain = "https://secret2"
-secret_builder.crypt("qwerty", "SAMPLE_2_PSK")
+secret_builder.coded = coder.encrypt("SamPle2")
 
 secret_2 = secret_builder.result
 
@@ -97,7 +98,7 @@ with assets.secret_loader(connection, sample_user) as loader:
     builder = assets.secret_builder(loaded)
 
     builder.domain = "https://other.com"
-    builder.crypt("qwerty", "sample_example")
+    builder.coded = coder.encrypt("OTHER SAMPLE")
 
     loader.update(builder.result)
 

+ 0 - 34
tests/006-secret_coder.py

@@ -1,34 +0,0 @@
-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.")