First run¶
The key¶
import paillier
pub, sec = paillier.generate_keypair(2048)
This generates safe primes — p = 2p′ + 1 with p′ prime as well.
That costs more than Blum primes (seconds against a fraction of one),
and it buys the non-smoothness of ord(hs) by construction rather
than statistically. Details: The scheme.
Keys shorter than 2048 bits are not generated at all: NIST SP 800-57 requires that length for 112-bit security, and anything shorter is not cryptography.
Encrypt, add, decrypt¶
values = [1.5, 2.25, -0.75]
blobs = [bytes(b) for b in paillier.encrypt_many(pub, values)]
total = paillier.add_many(pub, blobs)
paillier.decrypt(sec, total) # 3.0
encrypt_many encrypts in a batch and across all cores: on this
machine 6855 items per second against 1066 on one. Encrypting one at a
time in a Python loop throws that factor away.
add_many also takes a batch: one call into Rust for the whole thousand
terms instead of a thousand calls.
Encrypting under someone else's key¶
A party that only encrypts needs no private key — and no parameter at all beyond the modulus:
wire = bytes(pub.modulus_bytes()) # everything that goes over the wire
peer = paillier.PublicKey.from_n(wire)
blobs = [bytes(b) for b in paillier.encrypt_many(peer, [7.0, 8.0])]
from_n derives hs itself, from n alone. Importing it would
have been simpler, but then the encrypting side would have to trust a
foreign number that cannot be verified by computation at any price.
Building a peer key costs about 0.03 s at 3072 bits, so do it once per session, not once per message: the table of precomputed powers — the whole reason the scheme is fast — is built along with the key.
Scale¶
The default is 10^8. If your data is sign-constant and the sums are
long — bucket counters, squared gradients — pick a larger one:
blobs = paillier.encrypt_many(pub, values, scale_pow10=12)
The scale travels inside the ciphertext, so decryption reads it from
there and add_many refuses a batch that mixes scales. The two sides
have nothing to agree on. Details:
Encoding and scale.
What not to do¶
paillier.add_many(pub, blobs_from_another_key) # silently ruins the sum
There is no "this ciphertext was made under this key" check, and there
cannot be one: it does not follow from n alone. The refusal arrives
later, at the key holder, with no address on it. Keep a batch under one
key.