Merge commit '0b791f7a1d0b5798a90ffcd5bb11ddd55521a40b' as 'utils_v2'

This commit is contained in:
2024-12-04 11:41:40 +05:30
131 changed files with 101581 additions and 0 deletions
View File
+245
View File
@@ -0,0 +1,245 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Monday, 26th Aug., 2024
OBJECTIVE:
To provide an easy way to hash inputs.
REFERENCES:
1) Book: Full Stack Python Security - Dennis Byrne
DOWNLOADS:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# For hashing:
import hashlib
import hmac
import secrets
from bcrypt import hashpw, gensalt
# To work with buffers:
import io
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** CLASSES ***
# ***** ****
# *****************************************************************************************************************
class Hasher:
def __init__(
self,
algorithm = hashlib.sha256,
key = None
):
"""
hashes data or a message or a file. Uses HMAC if 'key' is specified, else performs simple hashing.
:param algorithm: The algorithm to use. SHA256 by default.
:param key: Specify this as either a string or as bytes to use HMAC. Leave as null for simple hashing.
"""
# Initialize the hasher:
self.__hasher = None,
self.__algorithm = algorithm
self.__hmac_key = key
if self.__hmac_key is not None:
self.__hmac_key = self.__hmac_key.encode("utf-8") if isinstance(self.__hmac_key, str) else self.__hmac_key
self.reset()
@staticmethod
def generate_key(byte_count = 32, url_safe = False):
"""
A mechanism to generate key/salt values.
NOTE: For a proper salt for passwords, I recommend using "generate_salt" method. It's far better.
:param byte_count: The number of bytes to have in the key. The hex output (as string) will have 2x the
characters.
:param url_safe: Set to True if you need the generated output to be a part of a URL.
:return: The generate key/salt.
"""
return secrets.token_urlsafe(byte_count) if url_safe else secrets.token_hex(byte_count)
@staticmethod
def generate_salt():
"""
Generate a salt to use while hashing things like passwords.
:return: The salt as bytes.
"""
return gensalt()
@staticmethod
def hash_password(password, salt):
"""
Hashes a password with the given salt.
:param password: The password to hash, either as a string or as bytes.
:param salt: The salt to hash the password with, either as a string or as bytes.
:return: The hashed string.
"""
return hashpw(
password = password.encode("utf-8") if isinstance(password, str) else password,
salt = salt.encode("utf-8") if isinstance(salt, str) else salt
)
@staticmethod
def compare_hashes(hash_0, hash_1):
"""
Compares two hashes to see if they match.
Comparison is done in constant time to avoid timing-based side-channel attacks.
:param hash_0: One of the hashes to compare.
:param hash_1: The other hash to compare.
:return: True if they match, else False.
"""
return hmac.compare_digest(hash_0, hash_1)
def reset(self):
"""
Resets the hasher by removing all the data that was fed into it.
:return: None.
"""
if self.__hmac_key is not None:
self.__hasher = hmac.new(
key = self.__hmac_key,
digestmod = self.__algorithm
)
else: self.__hasher = self.__algorithm()
def update(self, data):
"""
Adds data to the hash to update it.
:param data: The data to be hashed.
:return: None.
"""
data = data.encode("utf-8") if isinstance(data, str) else data
self.__hasher.update(data)
def digest(self):
"""
Returns the hexadecimal representation of the hash as a string.
:return: The hexadecimal representation of the hash as a string
"""
return self.__hasher.digest()
def hexdigest(self):
"""
Returns the hexadecimal representation of the hash as a string.
:return: The hexadecimal representation of the hash as a string
"""
return self.__hasher.hexdigest()
def hash_message(self, message, as_hex = True):
"""
Hashes one message and returns the result, and then resets the instance.
:param message: The data you want to hash.
:param as_hex: Invokes 'hexdigest' if True, else 'digest'.
:return: The hash of the message in either hexadecimal string form or binary form.
"""
self.update(message)
hash_result = self.hexdigest() if as_hex else self.digest()
self.reset()
return hash_result
def hash_file(self, file, chunk_size = 4096, as_hex = True):
"""
Hashes one file and returns the result, and then resets the instance.
:param file: The file you want to hash either as a path or as some buffer (like io.BytesIO).
:param chunk_size: The size of data (in bytes) that you would like to pick at one time.
:param as_hex: Invokes 'hexdigest' if True, else 'digest'.
:return: The hash of the file in either hexadecimal string form or binary form.
"""
# In case the file was given as a io.BytesIO buffer:
if isinstance(file, io.BytesIO):
file.seek(0)
while True:
chunk = file.read(chunk_size)
if not chunk: break
self.update(chunk)
# In case the file was given as a path:
else:
with open(file, "rb") as f:
for chunk in iter(lambda: f.read(chunk_size), b""):
self.update(chunk)
# Now we capture the results, reset the instance, and return the result:
hash_result = self.hexdigest() if as_hex else self.digest()
self.reset()
return hash_result
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
print(
Hasher.hash_password(
password = "mic test, mic test, 123",
salt = Hasher.generate_salt()
)
)
+156
View File
@@ -0,0 +1,156 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Tuesday, 10th Sept., 2024
OBJECTIVE:
To provide a way to generate and verify OTPs.
REFERENCES:
01. https://pyauth.github.io/pyotp/#
02. https://en.wikipedia.org/wiki/Google_Authenticator
DOWNLOADS:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")
# To work with OTPs:
import pyotp
import base64
import hashlib
# To work with date and time:
import time
import datetime
# For debugging:
from icecream import IceCreamDebugger
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** CLASSES ***
# ***** ****
# *****************************************************************************************************************
class HashedOTP:
def __init__(
self,
secret
):
"""
Used to generate and verify HMAC-based OTPs.
:param secret: The key to use to generate and verify OTPs.
"""
# Note down the input variables:
self.__secret = secret
self.__otp_client = pyotp.HOTP(secret)
@staticmethod
def generate_secret(message = None):
"""
Generate a secret key to then use to generate and verify the OTPs.
You may override the random generator by giving a "message" of any length.
:param message: A custom value to convert into a key. Avoid using this for better security, but this can be used
to generate keys based on user identifiers. THERE IS NO RANDOMNESS IF YOU USE THIS FEATURE. IT IS FOR
CONVENIENCE ONLY. NOT RECOMMENDED.
:return: The key (as a string) that can be used to generate and verify the OTPs.
"""
# If the user wants to generate a key from a custom input:
if message:
# Ensure we have a bytes object:
if not isinstance(message, (str, bytes)): message = str(message)
if isinstance(message, str): message = message.encode("utf-8")
# Hash the bytes object:
sha256_hash = hashlib.sha256()
sha256_hash.update(message)
hashed_key = sha256_hash.digest()
# Convert to base-32:
return base64.b32encode(hashed_key).decode("utf-8")
# If the user wants a totally random key:
else: return pyotp.random_base32()
def generate_otp(self, count: int):
"""
Generates the OTP at a particular step.
:param count: The step at which the OTP needs to be generated.
:return: The OTP string (6 digits).
"""
return str(self.__otp_client.at(count))
def verify_otp(self, otp, count: int):
"""
Verifies the claimed OTP.
:param otp: The OTP as claimed by the end user.
:param count: The step at which the OTP needs to be verified.
:return: True if the OTP is valid, else False.
"""
return self.__otp_client.verify(otp, counter = count)
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass
+135
View File
@@ -0,0 +1,135 @@
"""
AUTHOR:
Khushal P Soonderji
DATE:
Thursday, 25th Jul., 2024
OBJECTIVE:
To provide a set of data cleaning functions for inputs like phone numbers, emails, etc.
REFERENCES:
N/A
DOWNLOADS:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
sys.path.append(".")
sys.path.append("..")
# My utils:
from utils_v2.string import regex
# For random strings and tokens:
import string
import random
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
def file_name(input_string: str):
"""
Cleans up the string to allow it to safely become a filename.
:param input_string: The string that you want to make safe for using as a filename.
:return: The string that can safely be used as a filename.
"""
return regex.replace(
text = input_string.replace("\n", " "),
pattern = r"[^a-zA-Z0-9 \-_\.]",
substitute_text = ""
)
# ---------------------------------------------------------------------------------------------------------------------
def for_mongo(input_document):
"""
Sanitizes and disarms any JSON-like input that could be used for NoSQL-injection attacks.
:param input_document: The list or dict to be sanitized.
:return: The sanitized list or dict.
"""
# A special function that disarms any input string by dealing with special characters
# that Mongo may consider to be instructions:
def disarm(input_string):
input_string = regex.replace(
text = input_string,
pattern = r"[^a-zA-Z0-9,_\-\.\\\/:;'\(\) ]",
substitute_text = ""
)
return input_string
# Initially we assign the value of the input to the output:
sanitized_document = input_document
# Handle the case where the input is an array:
if isinstance(input_document, list):
sanitized_document = [for_mongo(document) for document in input_document]
# Handle the case when the input is a document:
elif isinstance(input_document, dict):
sanitized_document = {}
for k, v in input_document.items():
sanitized_document[disarm(k)] = v if type(v) not in [list, dict] else for_mongo(v)
# Done here:
return sanitized_document
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
pass