Package: tno.mpc.encryption-schemes.paillier
TNO MPC Lab - Encryption Schemes - PaillierThe TNO MPC lab consists of generic software components, procedures, and
functionalities developed and maintained on a regular basis to facilitate and
aid in the development of MPC solutions. The lab is a cross-project initiative
allowing us to integrate and reuse previously developed MPC functionalities to
boost the development of new protocols and solutions.The package tno.mpc.encryption_schemes.paillier is part of the TNO Python
Toolbox.Implementation of the Paillier encryption scheme. This library was designed to
facilitate both developers that are new to cryptography and developers that are
more familiar with cryptography.Supports:Positive and negative numbers, as well as fixed point encoding of numbers.Homomorphic addition of ciphertexts, negation of ciphertexts, and
multiplication of ciphertexts with integral scalars.Precomputation of the randomness needed for refreshing / generating fresh
encryptions.Custom class of warnings for making cryptographic suggestions to the
developer.Limitations in (end-)use: the content of this software package may solely be
used for applications that comply with international export control laws.This implementation of cryptographic software has not been audited. Use at your
own risk.DocumentationDocumentation of the tno.mpc.encryption_schemes.paillier package can be foundhere.InstallEasily install the tno.mpc.encryption_schemes.paillier package using pip:$python-mpipinstalltno.mpc.encryption_schemes.paillierNote:A significant performance improvement can be achieved by installing the GMPY2
library.$python-mpipinstall'tno.mpc.encryption_schemes.paillier[gmpy]'If you wish to use the tno.mpc.communication module you can use:$python-mpipinstall'tno.mpc.encryption_schemes.paillier[communication]'If you wish to run the tests you can use:$python-mpipinstall'tno.mpc.encryption_schemes.paillier[tests]'Basic usageBasic usage is as follows:fromtno.mpc.encryption_schemes.paillierimportPaillierif__name__=="__main__":# initialize Paillier with key_length of 2048 bits and fixed point precision of 3 decimalspaillier_scheme=Paillier.from_security_parameter(key_length=2048,precision=3)# encrypt the number 8.1ciphertext1=paillier_scheme.encrypt(8.1)# add 0.9 to the original plaintextciphertext1+=0.9# multiply the original plaintext by 10ciphertext1*=10# encrypt the number 10ciphertext2=paillier_scheme.encrypt(10)# add both encrypted numbers togetherencrypted_sum=ciphertext1+ciphertext2# ...communication...# decrypt the encrypted sum to 100decrypted_sum=paillier_scheme.decrypt(encrypted_sum)assertdecrypted_sum==100Running this example will show several warnings. The remainder of this
documentation explains why the warnings are issued and how to get rid of them
depending on the users' preferences.Fresh and unfresh ciphertextsAn encrypted message is called a ciphertext. A ciphertext in the current package
has a propertyis_freshthat indicates whether this ciphertext has fresh
randomness, in which case it can be communicated to another player securely.
More specifically, a ciphertextcis fresh if another user, knowledgeable of
all prior communication and all current ciphertexts marked as fresh, cannot
deduce any more private information from learningc.The package understands that the freshness of the result of a homomorphic
operation depends on the freshness of the inputs, and that the homomorphic
operation renders the inputs unfresh. For example, ifc1andc2are fresh
ciphertexts, thenc12 = c1 + c2is marked as a fresh encryption (no
rerandomization needed) of the sum of the two underlying plaintexts. After the
operation, ciphertextsc1andc2are no longer fresh.The fact thatc1andc2were both fresh implies that, at some point, we
randomized them. After the operationc12 = c1 + c2, onlyc12is fresh. This
implies that one randomization was lost in the process. In particular, we wasted
resources. An alternative approach was to have unfreshc1andc2then
compute the unfresh resultc12and only randomize that ciphertext. This time,
no resources were wasted. The package issues a warning to inform the user this
and similar efficiency opportunities.The package integrates naturally withtno.mpc.communicationand if that is
used for communication, its serialization logic will ensure that all sent
ciphertexts are fresh. A warning is issued if a ciphertext was randomized in the
process. A ciphertext is always marked as unfresh after it is serialized.
Similarly, all received ciphertexts are considered unfresh.Tailor behavior to your needsThe crypto-neutral developer is facilitated by the package as follows: the
package takes care of all bookkeeping, and the serialization used bytno.mpc.communicationtakes care of all randomization. The warnings can bedisabledfor a smoother experience.The eager crypto-youngster can improve their understanding and hone their skills
by learning from the warnings that the package provides in a safe environment.
The package is safe to use when combined withtno.mpc.communication. It
remains to be safe while you transform your code from 'randomize-early' (fresh
encryptions) to 'randomize-late' (unfresh encryptions, randomize before
exposure). At that point you have optimized the efficiency of the library while
ensuring that all exposed ciphertexts are fresh before they are serialized. In
particular, you no longer rely on our serialization for (re)randomizing your
ciphertexts.Finally, the experienced cryptographer can turn off warnings / turn them into
exceptions, or benefit from theis_freshflag for own purposes (e.g. different
serializer or communication).WarningsBy default, thewarningspackage prints only the first occurrence of a warning
for each location (module + line number) where the warning is issued. The user
may easilychange this behaviourto never see warnings:fromtno.mpc.encryption_schemes.paillierimportEncryptionSchemeWarningwarnings.simplefilter("ignore",EncryptionSchemeWarning)Alternatively, the user may pass"once","always"or even"error".Finally, note that some operations issue two warnings, e.g.c1-c2issues a
warning for computing-c2and a warning for computingc1 + (-c2).Advanced usageThebasic usagecan be improved upon by explicitly randomizing
at late as possible.fromtno.mpc.encryption_schemes.paillierimportPaillierif__name__=="__main__":paillier_scheme=Paillier.from_security_parameter(key_length=2048,precision=3)# unsafe_encrypt does NOT randomize the generated ciphertext; it is deterministic stillciphertext1=paillier_scheme.unsafe_encrypt(8.1)ciphertext1+=0.9ciphertext1*=10ciphertext2=paillier_scheme.unsafe_encrypt(10)# no randomness can be wasted by adding the two unfresh encryptionsencrypted_sum=ciphertext1+ciphertext2# randomize the result, which is now freshencrypted_sum.randomize()# ...communication...decrypted_sum=paillier_scheme.decrypt(encrypted_sum)assertdecrypted_sum==100As explainedabove, this implementation avoids
wasted randomization forencrypted_sumand therefore is more efficient.Speed-up encrypting and randomizingEncrypting messages and randomizing ciphertexts is an involved operation that
requires randomly generating large values and processing them in some way. This
process can be sped up which will boost the performance of your script or
package. The base packagetno.mpc.encryption_schemes.templatesprovides
several ways to more quickly generate randomness and we will show two of them
below.Generate randomness with multiple processes on the backgroundThe simplest improvement gain is to generate the required amount of randomness
as soon as the scheme is initialized (so prior to any call torandomizeorencrypt):fromtno.mpc.encryption_schemes.paillierimportPaillierif__name__=="__main__":paillier_scheme=Paillier.from_security_parameter(key_length=2048,precision=3)paillier_scheme.boot_randomness_generation(amount=5)# Possibly do some stuff hereformsginrange(5):# The required randomness for encryption is already prepared, so this operation is faster.paillier_scheme.encrypt(msg)paillier.shut_down()CallingPaillier.boot_randomness_generationwill generate a number of
processes that is each tasked with generating some of the requested randomness.
By default, the number of processes equals the number of CPUs on your device.Share Paillier scheme and generate randomness a prioriA more advanced approach is to generate the randomness a priori and store it.
Then, if you run your main protocol, all randomness is readily available. This
looks as follows. First, the key-generating party generates a public-private
keypair and shares the public key with the other participants. Now, every player
pregenerates the amount of randomness needed for her part of the protocol and
stores it in a file. For example, this can be done overnight or during the
weekend. When the main protocol is executed, every player uses the same scheme
(public key) as communicated before, configures the scheme to use the
pregenerated randomness from file, and runs the main protocol without the need
to generate randomness for encryption at that time. A minimal example is
provided below.frompathlibimportPathfromtypingimportListfromtno.mpc.communicationimportSerializationfromtno.mpc.encryption_schemes.templates.random_sourcesimportFileSourcefromtno.mpc.encryption_schemes.paillierimportPaillier,PaillierCiphertextdefinitialize_and_store_scheme()->None:# Generate schemescheme=Paillier.from_security_parameter(key_length=2048,precision=3)# Store without secret key for otherswithopen(Path("scheme_without_secret_key"),"wb")asfile:file.write(Serialization.pack(scheme,msg_id="",use_pickle=False))# Store with secret key for own usescheme.share_secret_key=Truewithopen(Path("scheme_with_secret_key"),"wb")asfile:file.write(Serialization.pack(scheme,msg_id="",use_pickle=False))# Tidy up to simulate real environment (program terminates)scheme.clear_instances()defload_scheme(path:Path)->Paillier:# Load scheme from diskwithopen(path,"rb")asfile:scheme_raw=file.read()returnSerialization.unpack(scheme_raw)[1]defpregenerate_randomness_in_weekend(scheme:Paillier,amount:int,path:Path)->None:# Generate randomnessscheme.boot_randomness_generation(amount)# Save randomness to comma-separated csvwithopen(path,"w")asfile:for_inrange(amount):file.write(f"{scheme.get_randomness()},")# Shut down processes gracefullyscheme.shut_down()defshow_pregenerated_randomness(scheme:Paillier,amount:int,path:Path)->None:# Configure file as randomness sourcescheme.register_randomness_source(FileSource(path))# Consume randomness from fileforiinrange(amount):print(f"Random element{i}:{scheme.get_randomness()}")defuse_pregenerated_randomness_in_encryption(scheme:Paillier,amount:int,path:Path)->List[PaillierCiphertext]:# Configure file as randomness sourcescheme.register_randomness_source(FileSource(path))# Consume randomness from fileciphertexts=[scheme.encrypt(_)for_inrange(amount)]returnciphertextsdefdecrypt_result(scheme:Paillier,ciphertexts:List[PaillierCiphertext])->None:# Show resultfori,ciphertextinenumerate(ciphertexts):print(f"Decryption of ciphertext{i}:{scheme.decrypt(ciphertext)}")if__name__=="__main__":AMOUNT=5RANDOMNESS_PATH=Path("randomness.csv")# Alice initializes, stores and distributes the Paillier schemeinitialize_and_store_scheme()# Tidy up to simulate real environment (second party doesn't yet have the Paillier instance)Paillier.clear_instances()# Bob loads the Paillier scheme, pregenerates randomness and encrypts the values 0,...,AMOUNT-1scheme_without_secret_key=load_scheme("scheme_without_secret_key")assert(scheme_without_secret_key.secret_keyisNone),"Loaded paillier scheme contains secret key! This is not supposed to happen."pregenerate_randomness_in_weekend(scheme_without_secret_key,AMOUNT,RANDOMNESS_PATH)show_pregenerated_randomness(scheme_without_secret_key,AMOUNT,RANDOMNESS_PATH)# Prints the following to screen (numbers will be different):# Random element 0: 663667452419034735381232312860937013...# Random element 1: ...# ...ciphertexts=use_pregenerated_randomness_in_encryption(scheme_without_secret_key,AMOUNT,RANDOMNESS_PATH)# Tidy up to simulate real environment (first party should use own Paillier instance)Paillier.clear_instances()# Alice receives the ciphertexts from Bob and decrypts themscheme_with_secret_key=load_scheme("scheme_with_secret_key")decrypt_result(scheme_with_secret_key,ciphertexts)# Prints the following to screen:# Decryption of ciphertext 0: 0.000# Decryption of ciphertext 1: 1.000# ...Statistical security for the one-time pad without modular reductionThe mathematics behind the statistical security of thesample_maskimplementation in this library can be foundhere.