Module solute.core

Expand source code
from hashlib import md5
from base64 import urlsafe_b64encode
from cryptography.fernet import Fernet
from PIL import Image
import numpy as np

try:
    from exceptions import InvalidModeError, ReadImageError, DataOverflowError, WriteImageError, CorruptDataError, PasswordError
    from utility import string_to_binary, binary_to_string
except ModuleNotFoundError:
    from solute.exceptions import InvalidModeError, ReadImageError, DataOverflowError, WriteImageError, CorruptDataError, PasswordError
    from solute.utility import string_to_binary, binary_to_string


# ENCRYPT-DECRYPT Text Data ---------------------------------------------------
def encrypt_decrypt(data_string:str, password:str, mode='encrypt') -> str:
    """Encrypts OR Decrypts data_string w.r.t password based on mode specified 
    Parameters:
        data_string: Text data for cryptic transformation.
        password: key string to lock/unlock data.
        mode: 
            'encrypt' --> encrypts the data
            'decrypt' --> decrypts the data
    Returns:
        Data string either encrypted or decrypted based on mode specified    
    """   
    # `password.encode()` --> conversion into bytes
    _hash = md5(password.encode())

    # hexadecimal hash value of hash object
    hash_value = _hash.hexdigest()     

    # Fernet key (bytes or str) –--> A URL-safe base64-encoded 32-byte key
    key = urlsafe_b64encode(hash_value.encode())
    cipher = Fernet(key)

    if mode == 'encrypt':
        data_bytes = data_string.encode()
        encrypted_bytes = cipher.encrypt(data_bytes)
        encrypted_data_string = encrypted_bytes.decode()
        return encrypted_data_string

    elif mode=='decrypt':
        encrypted_bytes = data_string.encode()
        decrypted_bytes = cipher.decrypt(encrypted_bytes)
        decrypted_data_string = decrypted_bytes.decode()
        return decrypted_data_string

    else:
        error_msg = "Invalid mode for encrypt_decrypt() \nexpected {'encrypt', 'decrypt'}"
        raise InvalidModeError(error_msg)


# ENCODER section -------------------------------------------------------------
def encode_img(input_img:str, text:str, output_img:str, password:str='') -> None:
    """ Create an Image encoded with text data.

    Parameters:
        input_img : path to input image file
        text : text data that needs to be encoded
        output_img : path to write output image 
        password : key to encrypt text data
    
    Returns:
        None
    """    
    if password:
        data = encrypt_decrypt(text, password, mode='encrypt')
    else:
        data = text 

    # process data: 32-bit info about length of data to be encoded
    data_length = bin(len(data))[2:]    # eg: '0b11101' --> '11101'
    data_length = data_length.zfill(32)

    # encode data prefixed with data-length
    bin_data = iter(data_length + string_to_binary(data))

    # read cover image 
    try:
        img = Image.open(input_img)
    except:
        raise ReadImageError(f"Image file {input_img} is inaccessible.")
    
    img_data = np.array(img)
    width, height = img.size
    total_pixels = height*width

    # each pixel of RGB --> 3 bytes --> 3 LSB bits --> 3 bits space per pixel to hide data
    encoding_capacity = 3*total_pixels

    # total bits in the data that needs to be hidden including 32 bits for specifying length of data
    data_bits = len(data_length) + len(string_to_binary(data))

    if data_bits > encoding_capacity:
        raise DataOverflowError("Data size too big to fit in this image!")

    encode_complete = False
    for x in range(height):
        for y in range(width):
            # reference of a mutable object 
            pixel = img_data[x][y]

            # each pixel has 3 LSB bits to hide data 
            for i in range(3):
                try:
                    d = next(bin_data)
                except StopIteration:
                    # no more binary data. objective accomplished
                    encode_complete = True
                    break

                # modify image bit according to data bit
                if d=='0':
                    pixel[i] &= ~(1)  # reset LSB bit

                elif d=='1':
                    pixel[i] |= 1  # set LSB bit

            # ---------------------------------------------------------------
            if encode_complete:
                break
        # -----------------------------------------------------------
        if encode_complete:
            break             
    
    try:
        encoded_img = Image.fromarray(img_data)
    except:
        raise WriteImageError("Error writing into new image")
        
    encoded_img.save(output_img)


# DECODER Section -------------------------------------------------------------
def decode_img(image_path:str, password:str='') -> str:
    """Extracts encoded text from the image and decrypts it with the password 

    Parameters:
        image: path to image
        password: key string to decrypt encoded text

    Returns:
        data_string
    """
    extracted_bits = []
    data_bits_length = None # length info

    try:
        img = Image.open(image_path)
    except:
        raise ReadImageError(f"Image file {image_path} is inaccessible.")
    
    img_data = np.array(img)

    # Image dimensions
    width, height = img.size 

    decode_complete = False 
    for x in range(height):
        for y in range(width):

            # current pixel RGB value
            pixel = img_data[x][y]

            # extract LSB bit of each RGB value
            for i in range(3):
                LSB_bit = str(pixel[i]&1)
                extracted_bits += LSB_bit

                if len(extracted_bits) == 32 and data_bits_length is None:
                    data_length = int(''.join(extracted_bits), base=2)
                    data_bits_length = data_length * 8 
                    # each character uses 8 bits

                    # Reset for actual data collection
                    extracted_bits = [] 
                
                # if all required bits are extracted, mark the process as completed
                elif len(extracted_bits) == data_bits_length:
                    decode_complete = True 
                    break 
            
            if decode_complete:
                break 
        if decode_complete:
            break
    if not decode_complete:
        raise CorruptDataError(f"Mismatched metadata and actual data. {image_path} is Corrupt!")
    
    binary_values = ''.join(extracted_bits)
    extracted_data = binary_to_string(binary_values)

    if password == '':
        return extracted_data
    else:
        try:
            data = encrypt_decrypt(extracted_data, password, mode='decrypt')
        except:
            raise PasswordError("Invalid Password. Please check.")
        return data 

Functions

def decode_img(image_path: str, password: str = '') ‑> str

Extracts encoded text from the image and decrypts it with the password

Parameters

image: path to image password: key string to decrypt encoded text

Returns

data_string

Expand source code
def decode_img(image_path:str, password:str='') -> str:
    """Extracts encoded text from the image and decrypts it with the password 

    Parameters:
        image: path to image
        password: key string to decrypt encoded text

    Returns:
        data_string
    """
    extracted_bits = []
    data_bits_length = None # length info

    try:
        img = Image.open(image_path)
    except:
        raise ReadImageError(f"Image file {image_path} is inaccessible.")
    
    img_data = np.array(img)

    # Image dimensions
    width, height = img.size 

    decode_complete = False 
    for x in range(height):
        for y in range(width):

            # current pixel RGB value
            pixel = img_data[x][y]

            # extract LSB bit of each RGB value
            for i in range(3):
                LSB_bit = str(pixel[i]&1)
                extracted_bits += LSB_bit

                if len(extracted_bits) == 32 and data_bits_length is None:
                    data_length = int(''.join(extracted_bits), base=2)
                    data_bits_length = data_length * 8 
                    # each character uses 8 bits

                    # Reset for actual data collection
                    extracted_bits = [] 
                
                # if all required bits are extracted, mark the process as completed
                elif len(extracted_bits) == data_bits_length:
                    decode_complete = True 
                    break 
            
            if decode_complete:
                break 
        if decode_complete:
            break
    if not decode_complete:
        raise CorruptDataError(f"Mismatched metadata and actual data. {image_path} is Corrupt!")
    
    binary_values = ''.join(extracted_bits)
    extracted_data = binary_to_string(binary_values)

    if password == '':
        return extracted_data
    else:
        try:
            data = encrypt_decrypt(extracted_data, password, mode='decrypt')
        except:
            raise PasswordError("Invalid Password. Please check.")
        return data 
def encode_img(input_img: str, text: str, output_img: str, password: str = '') ‑> None

Create an Image encoded with text data.

Parameters

input_img : path to input image file text : text data that needs to be encoded output_img : path to write output image password : key to encrypt text data

Returns

None

Expand source code
def encode_img(input_img:str, text:str, output_img:str, password:str='') -> None:
    """ Create an Image encoded with text data.

    Parameters:
        input_img : path to input image file
        text : text data that needs to be encoded
        output_img : path to write output image 
        password : key to encrypt text data
    
    Returns:
        None
    """    
    if password:
        data = encrypt_decrypt(text, password, mode='encrypt')
    else:
        data = text 

    # process data: 32-bit info about length of data to be encoded
    data_length = bin(len(data))[2:]    # eg: '0b11101' --> '11101'
    data_length = data_length.zfill(32)

    # encode data prefixed with data-length
    bin_data = iter(data_length + string_to_binary(data))

    # read cover image 
    try:
        img = Image.open(input_img)
    except:
        raise ReadImageError(f"Image file {input_img} is inaccessible.")
    
    img_data = np.array(img)
    width, height = img.size
    total_pixels = height*width

    # each pixel of RGB --> 3 bytes --> 3 LSB bits --> 3 bits space per pixel to hide data
    encoding_capacity = 3*total_pixels

    # total bits in the data that needs to be hidden including 32 bits for specifying length of data
    data_bits = len(data_length) + len(string_to_binary(data))

    if data_bits > encoding_capacity:
        raise DataOverflowError("Data size too big to fit in this image!")

    encode_complete = False
    for x in range(height):
        for y in range(width):
            # reference of a mutable object 
            pixel = img_data[x][y]

            # each pixel has 3 LSB bits to hide data 
            for i in range(3):
                try:
                    d = next(bin_data)
                except StopIteration:
                    # no more binary data. objective accomplished
                    encode_complete = True
                    break

                # modify image bit according to data bit
                if d=='0':
                    pixel[i] &= ~(1)  # reset LSB bit

                elif d=='1':
                    pixel[i] |= 1  # set LSB bit

            # ---------------------------------------------------------------
            if encode_complete:
                break
        # -----------------------------------------------------------
        if encode_complete:
            break             
    
    try:
        encoded_img = Image.fromarray(img_data)
    except:
        raise WriteImageError("Error writing into new image")
        
    encoded_img.save(output_img)
def encrypt_decrypt(data_string: str, password: str, mode='encrypt') ‑> str

Encrypts OR Decrypts data_string w.r.t password based on mode specified

Parameters

data_string: Text data for cryptic transformation. password: key string to lock/unlock data. mode: 'encrypt' –> encrypts the data 'decrypt' –> decrypts the data

Returns

Data string either encrypted or decrypted based on mode specified

Expand source code
def encrypt_decrypt(data_string:str, password:str, mode='encrypt') -> str:
    """Encrypts OR Decrypts data_string w.r.t password based on mode specified 
    Parameters:
        data_string: Text data for cryptic transformation.
        password: key string to lock/unlock data.
        mode: 
            'encrypt' --> encrypts the data
            'decrypt' --> decrypts the data
    Returns:
        Data string either encrypted or decrypted based on mode specified    
    """   
    # `password.encode()` --> conversion into bytes
    _hash = md5(password.encode())

    # hexadecimal hash value of hash object
    hash_value = _hash.hexdigest()     

    # Fernet key (bytes or str) –--> A URL-safe base64-encoded 32-byte key
    key = urlsafe_b64encode(hash_value.encode())
    cipher = Fernet(key)

    if mode == 'encrypt':
        data_bytes = data_string.encode()
        encrypted_bytes = cipher.encrypt(data_bytes)
        encrypted_data_string = encrypted_bytes.decode()
        return encrypted_data_string

    elif mode=='decrypt':
        encrypted_bytes = data_string.encode()
        decrypted_bytes = cipher.decrypt(encrypted_bytes)
        decrypted_data_string = decrypted_bytes.decode()
        return decrypted_data_string

    else:
        error_msg = "Invalid mode for encrypt_decrypt() \nexpected {'encrypt', 'decrypt'}"
        raise InvalidModeError(error_msg)