Skip to content

API

The paillier module. Everything it exposes is listed here.

generate_keypair(bits=3072)

Returns (PublicKey, SecretKey).

Generates two safe primes of bits/2 each and validates the resulting key. Keys shorter than 2048 bits are not generated: NIST SP 800-57 requires that length for 112-bit security.

It costs seconds and has a huge spread — anywhere from 1 to 10 seconds at 2048 bits on the same machine. It is a search: safe primes are rare, and the time to a hit is random.

pub, sec = paillier.generate_keypair(2048)

PublicKey

PublicKey.from_n(raw)

Builds a public key from the modulus alone — the thing that came over the wire. hs is derived right here, from n, rather than accepted from outside.

It costs about 0.03 s at 3072 bits, because the table of powers is built along with the key. Do this once per session.

Refuses an even modulus, one that is too short, and one that is too long. The length is cut off by raw byte count before any arithmetic: the modulus arrives from a peer, and 64 megabytes instead of a key must not cost the process anything.

modulus_bytes()

The modulus in bytes, most significant first. The only thing that travels to a peer.

exponent_bits

The length of the random exponent in bits — half the modulus length. Exposed so it can be checked by number rather than by guesswork.

plaintext_bound_bits

The largest accepted plaintext in magnitude, in bits: n/2, shrunk to reserve headroom for a sum.

encrypt_many(pub, values, scale_pow10=8)

Encrypts a list of numbers, returns a list of blobs.

Runs across all cores. Encrypting one at a time in a Python loop throws away a factor of six.

scale_pow10 is the power-of-ten exponent, from 0 to 18. It travels in every blob as the first byte. See Encoding.

Refuses NaN, infinities, numbers that overflow f64 once scaled, and numbers outside the key's admissible range.

Two keyword-only parameters name the encoding rule — rounding="half_away" | "half_even" and exact=False | True. Left unset they are the rule every release up to 0.7.1 applied, to the bit. See The rounding rule below.

add_many(pub, blobs)

Adds ciphertexts, returns a single blob.

Refuses: an empty batch; a batch longer than 2^20; a ciphertext outside [1, n²); a batch that mixes scales.

The 2^20 cap is not a guarantee

It is per call, and the result can be fed into a second call. What actually keeps sums in the group is the gap between the key floor (2^2026) and what is encodable from f64 at all (about 2^1024): a thousand bits short of overflow.

add_blocks(pub, blobs, blocks)

Sums MANY blocks over one array of ciphertexts, in parallel over the blocks. blocks is a list of index lists into blobs; the result is one blob per block, in the order the blocks were given.

Equivalent to [add_many(pub, [blobs[i] for i in block]) for block in blocks], byte for byte. It is faster on the shape it was written for, because the Python loop holds the GIL and re-parses every blob once per block that names it.

Refuses: an empty block; a block longer than 2^20; an index past the end of blobs; a block that mixes scales; a ciphertext outside [1, n²). An empty LIST of blocks is not an error — it is no sums, which is a different thing from an empty sum.

A ciphertext that no block names is never read, so it is never refused either. That is what makes the equivalence above hold for refusals as well as for values: the loop would not have read it.

Per block, not per call

Every limit and every refusal add_many applies to one sum, this applies to each block separately. Two blocks at two different scales are two sums and are allowed; two scales inside ONE block are not.

A repeated index is added again

[0, 0, 0] is three times that term. Indices are positions, not a set, and nothing here deduplicates them.

multiply_many(pub, blobs, scalars, *, scalar_scale_pow10=None)

Multiplies each ciphertext by a known scalar — E(x) → E(k·x) — and returns a list of blobs, paired with the input by position.

The scale of a product is the sum of the two scales, and the header of the returned blob says so, so decrypt divides by the right thing without being told. scalar_scale_pow10 defaults to the scale of the ciphertext, so the ordinary case is 8 + 8 = 16.

Refuses: a length mismatch between blobs and scalars; a scalar that is NaN or infinite; a scalar that encodes to zero; a scalar past the fixed exponent width; a product scale above 1e18; a ciphertext outside [1, n²) or not invertible.

Multiplication does not compose at the default

A second multiplication would be 16 + 16 = 32 — refused. Chaining means lowering scalar_scale_pow10 on both calls.

For the same reason a product cannot be added to an ordinary ciphertext: add_many refuses a mixed batch. Encrypt the companions at the product's scale — encrypt_many(pub, values, scale_pow10=16).

A scalar that encodes to zero is refused, an exact zero included

E(x)^0 is the constant 1: the value is destroyed and the result becomes a two-byte blob anyone can recognise. Where the scalar is a share or a weight, a vanishing one means "this bucket is empty", and publishing that as a distinctive blob is a leak rather than a result. Encrypt a zero if a zero is what is wanted.

The exponent runs at a FIXED width

The scalar is usually the caller's own secret, so this takes the secret-exponent path, as decryption does. That hides the exponent's value but not its length — and its length is the magnitude of the scalar. So the exponent is offset to a constant width and the offset divided out afterwards, at the cost of a second exponentiation.

The consequence is the refusal above: a scalar that does not fit the fixed width is rejected rather than run at a wider exponent. At the default scale that admits |k| ≤ 1.8e11.

The sign is hidden too, and by construction rather than by care: the offset makes every exponent positive, so both exponentiations and the inversion run unconditionally. There is no branch on the sign to observe.

An earlier draft of this page claimed the opposite — that a negative scalar costs an inversion and a positive one does not. That described a naive implementation, not this one, and it was caught by review rather than by a test.

Do NOT transmit a raw product if the scalar is secret

The output is E(x)^k and nothing else — a deterministic function of the input ciphertext and the scalar. Anyone who sees BOTH the input and the output recovers k by trying candidates: n is public, so each guess costs one exponentiation, and a scalar that is a share or a small weight has few candidates worth trying.

Which means the fixed-width construction above protects a side door while this one stands open. It is worth having — timing is available to a neighbour on the same machine, who may never see the ciphertexts — but it is not what keeps k secret from someone watching the wire.

Use rerandomize before transmitting, or sum the products with something the other side did not send. The analytics exchange this feature was built for does the latter, which is why the library does not do it for you: it cannot know which of your ciphertexts are about to leave.

multiply_many_public(pub, blobs, scalars, *, scalar_scale_pow10=None)

The same product as multiply_many, computed for a caller who states that the scalar is not a secret.

Byte-for-byte identical results. The same refusals, plus one of its own. About twelve times faster, and one property fewer.

Which one to reach for

multiply_many keeps the exponent's width constant, at the cost of two exponentiations and a modular inversion per product. That is right when the scalar is what the exchange conceals — an analytics bucket share, say.

This one runs a single windowed exponentiation. Reach for it when the scalar is the multiplying party's own data, never leaves it, and neither do the products. Vertical linear training is the case it was added for: the passive party multiplies encrypted residuals by its own features and sends only their masked sum.

What is traded, measured

The time depends on the scalar's magnitude. On a 2048-bit key:

exponent this function multiply_many
1 bit 0.0046 ms ~2.1 ms
64 bits 0.3200 ms ~2.2 ms

One observation separates a small scalar from a large one at about 93% accuracy; twenty-seven reach 99%. If the party receiving the answer must not learn the magnitudes, this is the wrong function — and that party is usually the one being defended against, so the question is worth asking rather than assuming.

Typical shape, batches of 200 at ~29-bit exponents: 0.181 ms against 2.193 ms, a factor of 12.1.

Bounds

|k| < 2^53 after encoding, and this range is narrower than multiply_many's, not wider. That function refuses an encoded |k| ≥ 2^64; 2^53 is below 2^64 at every scale. Measured across 3000 combinations of scale and magnitude: 108 inputs this path refuses and the other accepts, none the other way round.

The two bounds differ in kind. 2^64 was a timing requirement — every exponent had to be the same width. 2^53 is where an f64 stops holding every integer, so past it the value multiplied in is not the value you named: 10^20 arrives as 1.9999999999999997e20. multiply_many rounds there silently; this refuses.

At the default scale of 1e8 that means |k| ≤ 9.0e7. A unix timestamp (1.7e9), a population count, a revenue figure — all accepted by multiply_many, all refused here. The way through is a lower scalar_scale_pow10, trading fractional precision for range: at 1e2 the ceiling is 9.0e13.

The plaintext space (2^2027 at a 2048-bit key) never binds either way, and nothing wraps silently: every out-of-range path ends in a refusal.

Refusals

Everything multiply_many refuses, plus:

  • a ciphertext sharing a factor with n — for every scalar. pow_mod reports this only when the exponent is negative, because only then does it need the inverse. Left to it, the same input was accepted on positive scalars and rejected on negative ones. The check is explicit here.

The rounding rule: rounding= and exact=

encrypt_many, multiply_many and multiply_many_public all take

rounding="half_away"   # or "half_even"
exact=False            # or True

as keyword-only arguments, applied to the values or the scalars they encode. Unset, the result is what 0.7.1 computed, bit for bit.

The 0.7.1 rule is two roundings: the product v * 10**e is rounded to the nearest f64, and that f64 is rounded to an integer, ties away from zero.

rounding exact the integer
"half_away" False the default: f64 product, ties away from zero
"half_even" False f64 product, ties to the even integer
"half_away" True the exact product v · 10^e, ties away from zero
"half_even" True the exact product v · 10^e, ties to the even integer

In exact mode v is the rational number its bits denote — 0.1 is 0.1000000000000000055511… — multiplied by 10^e in the integers and rounded once. The two modes give the same integer whenever the f64 product is exact and does not straddle a midpoint; they part when it is not:

paillier.encrypt_many(pub, [0.1], scale_pow10=18)              # holds 100000000000000000
paillier.encrypt_many(pub, [0.1], scale_pow10=18, exact=True)  # holds 100000000000000006

and above 2^53/10^e, where the default keeps only what the f64 grid holds and exact mode keeps every bit of v. Exact mode also does not overflow where the product would: 1e300 at 1e18 is refused by the default and encoded by exact mode.

Exact mode encodes the BINARY value, not a decimal literal: if what you have is the decimal 0.1, what reaches the library is already the nearest double. For values that are integers to begin with, use encrypt_integers.

The rule decides which integer a value becomes, never which integers are admissible: NaN and infinities are refused under every rule with the same text, a scalar still encoding to zero is refused, and the 2^64 width of multiply_many and the 2^53 bound of multiply_many_public apply in every mode. An unknown rule name is a ValueError.

encrypt_integers(pub, values, scale_pow10=0)

Encrypts Python integers as they are: the plaintext is the integer. Nothing passes through f64, so 2**53 + 1 stays 2**53 + 1 — which encrypt_many(pub, [float(2**53 + 1)], scale_pow10=0) cannot do.

"Integer" is what operator.index accepts: int, bool, numpy integer scalars, anything with __index__. A float is a TypeError, even an integral one.

scale_pow10 (0..18) is recorded, not applied: it goes into the header and says what the integer means. encrypt_integers(pub, [150], scale_pow10=2) decrypts to 1.5. The integer is not multiplied by anything.

Bound: |x| ≤ n/2^21 — the same bound encrypt_many applies, which a float can never reach and an integer can. Past it, a ValueError; a value is never reduced modulo n. A huge integer is refused by its bit_length before its digits are copied.

Blobs from here and from encrypt_many at the same scale are the same kind of ciphertext and add together.

multiply_integers(pub, blobs, scalars, scalar_scale_pow10=0, *, scalar_bits=64)

Multiplies each ciphertext by a Python integer scalar, exactly, with the scalar kept secret: E(x) → E(k·x). The same product as multiply_many, without a float in the way — byte for byte the same wherever a float can name k.

scalar_scale_pow10 (default 0) is recorded: the product's header is the ciphertext's scale plus it, at most 18. The default leaves the scale as it was: multiplying by a count does not change the units.

The width is declared, not measured

The timing property is multiply_many's: every exponentiation of the call runs at scalar_bits + 2 bits whatever the scalar, the offset divided back out, so neither the value, the length nor the sign of k changes the exponentiations.

For a float scalar the scheme could fix that width at 64 bits. For an integer of any size it cannot, so the caller declares it: scalar_bits, default 64 — the width multiply_many runs at — and at most plaintext_bound_bits. A scalar with |k| ≥ 2^scalar_bits is refused, not run wider.

The width shows in the timing, exactly as it does for multiply_many. Choose it from what the scalars may be, once per call; never compute it from what they are.

What the width covers is the exponentiation. Reading each Python int takes one code path whatever its value, but Python's own work on a variable-length number follows its number of digits — as multiply_many's float encoding follows the float's — at a cost far below one exponentiation, and not zero.

Refuses: a length mismatch; a scalar that is not an integer (TypeError); a zero scalar, for the reason multiply_many gives; a scalar past the declared width; a scalar past the plaintext bound n/2^21; scalar_bits outside 1..plaintext_bound_bits; a product scale past 1e18; a ciphertext outside [1, n²) or not invertible.

What is not and cannot be checked: that k·x itself stays inside the plaintext space. x is encrypted.

The output is E(x)^k and nothing else — deterministic. The warning under multiply_many about transmitting a raw product applies here in full: rerandomize before it leaves.

multiply_integers_public(pub, blobs, scalars, scalar_scale_pow10=0)

The same product for a scalar the caller declares public: one windowed exponentiation at the scalar's own length. The time follows log2|k| — the trade multiply_many_public makes, to be chosen by the same question.

There is no 2^53 bound here. That bound exists on the float path because a float past it is not the integer the caller meant; a Python int is. The bound is the plaintext space, 0 < |k| ≤ n/2^21.

Refuses everything multiply_integers refuses except the width, and a ciphertext sharing a factor with n is refused for every scalar, whatever its sign.

rerandomize(pub, blobs)

Same plaintext, different bytes. Each ciphertext is multiplied by a fresh encryption of zero.

Runs across all cores. Costs about one encryption per ciphertext, because that is what it is: one exponentiation at the full exponent length.

Refuses an empty blob, an unknown scale exponent, and a ciphertext outside [1, n²). The scale byte passes through unchanged.

Why this is a separate call and not a flag

The homomorphic operations are deterministic: add_many of the same terms gives the same bytes, a one-term sum gives its input back verbatim, multiply_many gives exactly E(x)^k. Whoever knows the inputs can confirm a guess about the operation by recomputing it.

That only matters when the result LEAVES. Inside a computation the property buys nothing and costs an encryption apiece — so it is the caller who applies it, at the point where they know what is being sent, which the library cannot know.

A flag was considered. On by default it charges every caller for something most do not need; off by default it is left off exactly where it costs most.

It hides WHICH ciphertext, not the value

Re-randomising does nothing against a party that can decrypt. If the recipient of your result holds the private key, they read the plaintext, and this changes nothing about that.

decrypt(sec, blob)

Returns a float. The scale is taken from the blob itself.

Refuses an empty blob, an unknown scale exponent, and a value that is not a ciphertext under this key.

There is no 'this ciphertext under this key' check

Nor can there be: it does not follow from n alone. A ciphertext made under a different key will usually lead to a refusal — but not always.

decrypt_many(sec, blobs)

Decrypts a batch across all cores and returns the plaintexts as decimal strings, in the order given.

Strings, not floats, because the values this is for are aggregates: a homomorphic sum passes 2^53 on an ordinary batch, and above that an f64 holds only the even integers. decrypt would return a number that is not flagged, not out of range and not implausible — merely a different one.

Refuses a blob carrying a non-zero scale, and a value that is not a ciphertext under this key. The scale is refused rather than divided out: dividing is what puts the rounding back.

Use decrypt for a magnitude

A scaled value is a magnitude, and a magnitude is what decrypt returns. This one is for callers that will add, compare or re-encode the result, where a double rounding through binary64 stops being the identity well below 2^53.

decrypt_exact_many(sec, blobs)

Decrypts a batch across all cores and returns, per blob, a pair (numerator, pow10):

  • numerator — the plaintext integer, a Python int, any size, signed;
  • pow10 — the blob's own scale byte, 0..18.

The value is numerator / 10**pow10, and nothing has been divided. This is the exact counterpart of both decrypt, which divides in f64, and decrypt_many, which refuses any scale but zero. A sum at the default scale of 1e8 passes 2^53 on an ordinary batch, and this is the way back that loses nothing.

from fractions import Fraction
from decimal import Decimal

m, e = paillier.decrypt_exact_many(sec, [total])[0]
Fraction(m, 10**e)          # exact
Decimal(f"{m}e-{e}")        # exact; Decimal ARITHMETIC then rounds to its context

The pair is not reduced: (150, 2) stays (150, 2), because e is the scale the ciphertext carries, which matters to whoever sums it further.

Where decrypt is exact — |numerator| < 2^53 — the two agree: decrypt(sec, blob) == float(Fraction(m, 10**e)).

Refuses an empty blob, an unknown scale byte, and a value that is not a ciphertext under this key, naming the blob by position.

check_blobs(pub, blobs, scale_pow10)

Returns None, or raises ValueError naming the first blob, by position, that fails:

  • its header is not exactly scale_pow10 (itself 0..18);
  • it is empty, or a header with no ciphertext;
  • its ciphertext is not in the canonical form every function here writes — minimal length, most significant byte first, so no leading zero byte;
  • its ciphertext is longer than n² takes, or its value is not below n².

Every other function reads the scale from the blob and trusts it. That is what lets a peer encrypt without agreeing a scale, and also why a flipped header byte returns a plausible wrong number. A caller that receives blobs from outside and knows the scale they must carry states it here first.

Not checked, as elsewhere

That the value is invertible modulo n² — a gcd per blob costs more than a sum of them, the reasoning add_many gives — and that it was made under this key, which does not follow from n.

__version__

The package version, taken from Cargo.toml at build time rather than typed in a second place.

Blob format

[1 byte: scale exponent] [ciphertext, most significant byte first]

The ciphertext is written at minimal length, not padded to a fixed width: measured {512: 2, 513: 398} over four hundred encryptions with a 2048-bit key. You cannot slice the buffer at a constant stride.