How to Encrypt Float Numbers with Public Key Algorithms in Python

In the ever-evolving landscape of data security, the need to protect sensitive information, including numeric data, is paramount. While public key algorithms have long been recognized as stalwarts in safeguarding digital communication, their application to floating-point numbers poses unique challenges. Public key algorithms, inherently designed for integers and modular arithmetic, demand a thoughtful adaptation to handle the intricacies of floating-point precision. In this blog post, we delve into the intriguing realm of encrypting float numbers using public key algorithms, exploring the intersection of numerical accuracy and cryptographic prowess. To bring these concepts to life, we’ll embark on a practical journey using Python, utilizing the powerful capabilities of the LightPHE library. We will use the most common public key algorithms in our implementations: RSA, ElGamal and Paillier. Through real-world examples and code implementations, we’ll unravel the intricacies of securing floating-point data, bridging the theoretical aspects of cryptography with hands-on Python applications.

Round Skeleton Watch From Pexels

Precision-Infused Encryption: Navigating the Intricacies of Floating-Point Security

In the quest to encrypt floating-point numbers with public key algorithms, a strategy reminiscent of COBOL copybooks emerges—one that incorporates precision values as an integral part of the number itself. This innovative approach involves augmenting the integer representation of a floating-point number with its precision during the encryption process.


🙋‍♂️ You may consider to enroll my top-rated cryptography course on Udemy

Cryptography Basiscs From Scratch In Python

Cobol Copybook

For instance, salary and commission fields are storing 5 digits for the integral part and 2 digits for the decimal part in the following example. In other words, the number 100 will be stored as 0010000 in the mainframe.

So, encrypting 1.05 transforms into encrypting 105, with the understanding that the precision of two decimal places is implied. The decryption counterpart adeptly extracts this precision, dividing the decrypted integer by the appropriate power of ten to reconstruct the original floating-point value. However, it’s crucial to acknowledge the inherent trade-off in this methodology. Let’s implement this with RSA algorithm.

from lightphe import LightPHE

# build a RSA cryptosystem - this will generate key pair
cs = LightPHE(algorithm_name = "RSA")

# plaintext
m = 1.5

# public factor
factor = 100

# making plaintext integer
m_prime = int(m * factor)

# encryption
c = cs.encrypt(m_prime)

# proof of work
assert cs.decrypt(c) / factor == m

While it succeeds admirably for numbers like 1.05, there exists a limitation when encrypting values such as 1.005, where fractional precision nuances might be lost in the encryption process. You have to update your copybook! Balancing the advantages and drawbacks, this nuanced encryption technique showcases the delicate art of securing floating-point data with a touch of ingenuity.

Modular Mastery: Encrypting Floating-Point Numbers with Precision and Homomorphism

In the realm of encrypting floating-point numbers within the framework of public key algorithms, a fascinating journey into modular arithmetic unveils innovative strategies. Public key algorithms, inherently rooted in modular arithmetic, prescribe a realm where plaintexts are confined to the finite range of 0 to n-1, where n is the modulus. This opens a unique avenue for converting floating-point numbers to integers through modular arithmetic. By taking, for instance, 1.05 mod n, a float number is transformed into an integer (105), revealing an opportunity for encryption. Leveraging the multiplicative inverse of the precision factor (100 in this example), the encryption process involves multiplying this inverse by the float value’s integer representation. However, a caveat emerges in the decryption phase, where the result is an integer, potentially complicating the restoration of the original floating-point number. Let’s implement this with ElGamal algorithm.

from typing import Tuple
from lightphe import LightPHE

# build a ElGamal cryptosystem - this will generate key pair
cs = LightPHE(algorithm_name = "ElGamal")

modulo = cs.cs.plaintext_modulo

def fractionize(value: float, modulo: int) -> Tuple[int, int]:
    decimal_places = len(str(value).split(".")[1])
    scaling_factor = 10**decimal_places
    integer_value = int(Decimal(value) * Decimal(scaling_factor)) % modulo
    return integer_value, scaling_factor

# plaintext
m = 1.05

# represent plaintext as dividend and divisor
m_dividend, m_divisor = fractionize(m, modulo)

# find the multiplicative inverse of divisor
m_prime = ( m_dividend * pow(m_divisor, -1, modulo) ) % modulo

# encrypt m_prime
c = cs.encrypt(m_prime)

# proof of work
assert cs.decrypt(c) == m_prime

Yet, within the realm of multiplicative homomorphic algorithms, an intriguing prospect arises—multiplying an encrypted value by a factor akin to 1.05 can symbolize a percentage increase, offering a unique perspective on the delicate interplay between encryption, precision, and numerical homomorphism.

from lightphe import LightPHE

# build a ElGamal cryptosystem - this will generate key pair
cs = LightPHE(algorithm_name = "ElGamal")

# define plaintexts
m1 = 10000
m2 = 1.05

# calculate ciphertexts
c1 = cs.encrypt(m1)
c2 = cs.encrypt(m2)

# homomorphic operation
c3 = c1 * c2

# proof of work
assert cs.decrypt(c3) == m1 * m2

As you noticed, if you send a float number to encrypt function of LightPHE, then it will apply this approach.

Fractional Fortification: Securing Floating-Point Numbers through Dividend-Divisor Encryption

In the pursuit of fortifying the encryption of floating-point numbers, a novel approach arises—fractionizing these numbers into their constituent parts: dividend, divisor, and sign. By representing a float number as a combination of these elements, each component is encrypted independently during the encryption process. This meticulous separation ensures enhanced security, as the encrypted dividend, divisor, and sign are treated as distinct entities. In the decryption phase, the individual encrypted components are unveiled, providing a clear path to the restoration of the original floating-point number. This method not only bolsters the confidentiality of the numerical data but also simplifies the retrieval process, offering a nuanced solution in the intricate landscape of securing and decrypting floating-point values. Let’s implement this with Paillier algorithm.

from typing import Tuple
from lightphe import LightPHE

# build a Paillier cryptosystem - this will generate key pair
cs = LightPHE(algorithm_name = 'Paillier')

# find the modulo
modulo = cs.cs.plaintext_modulo

def fractionize(value: float, modulo: int) -> Tuple[int, int]:
    decimal_places = len(str(value).split('.')[1])
    scaling_factor = 10**decimal_places
    integer_value = int(Decimal(value) * Decimal(scaling_factor)) % modulo
    return integer_value, scaling_factor

# define plaintext
m = 1.05

# represent plaintext as dividend and divisor
m_dividend, m_divisor = fractionize(m, modulo)

# encrypt dividend and divisor seperately
m_dividend_encrypted = cs.encrypt(m_dividend)
m_divisor_encrypted = cs.encrypt(m_divisor)

# proof of work
x = cs.decrypt(m_dividend_encrypted)
y = cs.decrypt(m_divisor_encrypted)
assert x / y == m

Pros of this approach is that decimal places are calculated dynamically according to the float number. In other words, you can restore any float number after encryption & decryption process. Besides, you can additionally encrypt the sign of the plaintext in this approach.





LightPHE has a shortcut for this approach. If you define your float number as an item of tensor, then the library will apply this approach as illustrated below.

from lightphe import LightPHE

# build a Paillier cryptosystem - this will generate key pair
cs = LightPHE(algorithm_name = 'Paillier')

# define plaintext
m = [1.05]

# encryption
c = cs.encrypt(m_dividend)

# proof of work
assert cs.decrypt(c) == m

Conclusion

In this exploration of encrypting floating-point numbers within the realm of public key algorithms, we’ve ventured into three distinct methodologies, each with its own merits and considerations. “Precision-Infused Encryption” showcased the artful integration of precision values, offering a delicate balance between numerical accuracy and cryptographic robustness. “Modular Mastery” unveiled the transformative power of modular arithmetic in converting and encrypting floating-point numbers, ushering in a unique set of challenges in the decryption phase. Finally, “Fractional Fortification” introduced a novel approach, fractionizing float numbers into dividend, divisor, and sign for independent encryption, promising both heightened security and simplified restoration during decryption. As we navigate this encryption tapestry, it becomes evident that the choice of methodology depends on the specific needs of the application, weighing the trade-offs between precision, ease of decryption, and security. The encryption journey for floating-point numbers is dynamic, offering a spectrum of solutions to cater to the diverse landscape of data security requirements.


Like this blog? Support me on Patreon

Buy me a coffee